From 78906c1d47ace74fe8c5c76b7d7ad80ef94ee039 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 26 Oct 2022 16:27:41 +0200 Subject: [PATCH 01/27] Use environment variable to enable API auth Setting VULNERABLECODEIO_REQUIRE_AUTHENTICATION will require auth with an API key Signed-off-by: Philippe Ombredanne --- vulnerablecode/settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index c8e9550d3..9de4645f0 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -83,6 +83,7 @@ "drf_spectacular_sidecar", ) + MIDDLEWARE = ( "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -294,6 +295,9 @@ "TAGS_SORTER": False, } +if not VULNERABLECODEIO_REQUIRE_AUTHENTICATION: + REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.AllowAny",) + if DEBUG_TOOLBAR: INSTALLED_APPS += ("debug_toolbar",) @@ -319,6 +323,3 @@ INTERNAL_IPS = [ "127.0.0.1", ] - -if not VULNERABLECODEIO_REQUIRE_AUTHENTICATION: - REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.AllowAny",) From c282203b803e828e9a234a18a16eaffb10257a96 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Thu, 27 Oct 2022 13:24:58 +0200 Subject: [PATCH 02/27] Improve docstring for user signal Signed-off-by: Philippe Ombredanne --- vulnerabilities/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 30434cbfd..29c202139 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -12,6 +12,7 @@ import logging from contextlib import suppress +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import UserManager from django.core import exceptions @@ -23,7 +24,8 @@ from django.db.models import Q from django.db.models.functions import Length from django.db.models.functions import Trim -from django.urls import reverse +ffrom django.dispatch import receiver +rom django.urls import reverse from packageurl import PackageURL from packageurl.contrib.django.models import PackageURLMixin from packageurl.contrib.django.models import PackageURLQuerySet From 78625e7284e2526c9258d89c9c16e837f8e187c8 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Thu, 27 Oct 2022 13:28:39 +0200 Subject: [PATCH 03/27] Enable the admin This is handy for data browsing Signed-off-by: Philippe Ombredanne --- vulnerablecode/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index a35f5c22b..bd0a19e15 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -26,6 +26,7 @@ from vulnerabilities.views import PackageSearch from vulnerabilities.views import VulnerabilityDetails from vulnerabilities.views import VulnerabilitySearch +from vulnerabilities.views import schema_view from vulnerablecode.settings import DEBUG_TOOLBAR From 5f72d37189409b4b9f610f39f599c227f9586e94 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Thu, 27 Oct 2022 13:29:02 +0200 Subject: [PATCH 04/27] Add new command to create API-only users Signed-off-by: Philippe Ombredanne --- .../management/commands/create-api-user.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 vulnerabilities/management/commands/create-api-user.py diff --git a/vulnerabilities/management/commands/create-api-user.py b/vulnerabilities/management/commands/create-api-user.py new file mode 100644 index 000000000..01750b7df --- /dev/null +++ b/vulnerabilities/management/commands/create-api-user.py @@ -0,0 +1,90 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/nexB/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +import getpass + +from django.contrib.auth import get_user_model +from django.contrib.auth.password_validation import validate_password +from django.core import exceptions +from django.core.management.base import BaseCommand +from django.core.management.base import CommandError +from rest_framework.authtoken.models import Token + +""" +Create a basic API-only user based on an email. +""" + + +class Command(BaseCommand): + help = "Create a basic passwordless user with an API key for sole API authentication usage." + requires_migrations_checks = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.UserModel = get_user_model() + self.username_field = self.UserModel._meta.get_field(self.UserModel.USERNAME_FIELD) + + def add_arguments(self, parser): + parser.add_argument("--email", help="Specifies the email for the user.") + parser.add_argument( + "--first-name", + default="", + help="First name.", + ) + parser.add_argument( + "--last-name", + default="", + help="Last name.", + ) + + def handle(self, *args, **options): + email = options["email"] + email = self.UserModel._default_manager.normalize_email(email) + username = email + + error_msg = self._validate_username(username) + if error_msg: + raise CommandError(error_msg) + + first_name = options["first_name"] or None + last_name = options["last_name"] or None + + password = None + user = self.UserModel._default_manager.create_user( + username=username, + email=email, + password=password, + first_name=first_name, + last_name=last_name, + ) + # this esnure that this is not a valid password + user.set_unusable_password() + user.save() + + token, _ = Token._default_manager.get_or_create(user=user) + + msg = f"User {username} created with API key: {token.key}" + self.stdout.write(msg, self.style.SUCCESS) + + def _validate_username(self, username): + """ + Validate username. If invalid, return a string error message. + """ + if self.username_field.unique: + try: + self.UserModel._default_manager.get_by_natural_key(username) + except self.UserModel.DoesNotExist: + pass + else: + return "Error: That email username is already taken." + + try: + self.username_field.clean(username, None) + except exceptions.ValidationError as e: + return "; ".join(e.messages) From bc96503423eaab6bb12c80dc55a6f6c3c90640db Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Thu, 27 Oct 2022 13:58:20 +0200 Subject: [PATCH 05/27] Add minimal command to create API-only users Also add minimal API auth and configuration documentation Signed-off-by: Philippe Ombredanne --- docs/source/api.rst | 15 ++++----------- .../management/commands/create-api-user.py | 4 ++-- .../tests/test_create_api_user_command.py | 14 +++++--------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index c779784a5..fd991403a 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -4,12 +4,6 @@ API overview ======================== -Browse the Open API documentation ------------------------------------- - -- https://public.vulnerablecode.io/api/docs/ for documentation with Swagger -- https://public.vulnerablecode.io/api/schema/ for the OpenAPI schema - Enable the API key authentication ------------------------------------ @@ -25,14 +19,14 @@ Create an API key-only user This can be done in the admin and from the command line:: - $ ./manage.py create_api_user --email "p4@nexb.com" --first-name="Phil" --last-name "Goel" + $ ./manage.py create-api-user --email "p4@nexb.com" --first-name="Phil" --last-name "Goel" User p4@nexb.com created with API key: ce8616b929d2adsddd6146346c2f26536423423491 Access the API using curl ----------------------------- - curl -X GET -H 'Authorization: Token ' https://public.vulnerablecode.io/api/ + curl -X GET -H 'Authorization: Token ' http://public.vulnerablecode.io/api/ API endpoints @@ -43,8 +37,7 @@ There are two primary endpoints: - packages/: this is the main endpoint where you can lookup vulnerabilities by package. -- vulnerabilities/: to lookup by vulnerabilities +- vulnerabilities.: for lookup by vulnerabilities -And two secondary endpoints, used to query vulnerability aliases (such as CVEs) -and vulnerability by CPEs: cpes/ and aliases/ +And two secondary endpoints, used to query aliases and CPEs: cpes/ and alias/ diff --git a/vulnerabilities/management/commands/create-api-user.py b/vulnerabilities/management/commands/create-api-user.py index 01750b7df..e4227a93c 100644 --- a/vulnerabilities/management/commands/create-api-user.py +++ b/vulnerabilities/management/commands/create-api-user.py @@ -52,8 +52,8 @@ def handle(self, *args, **options): if error_msg: raise CommandError(error_msg) - first_name = options["first_name"] or None - last_name = options["last_name"] or None + first_name = options["first_name"] or "" + last_name = options["last_name"] or "" password = None user = self.UserModel._default_manager.create_user( diff --git a/vulnerabilities/tests/test_create_api_user_command.py b/vulnerabilities/tests/test_create_api_user_command.py index 6c54fca24..cd78a699b 100644 --- a/vulnerabilities/tests/test_create_api_user_command.py +++ b/vulnerabilities/tests/test_create_api_user_command.py @@ -19,7 +19,7 @@ class TestCreateApiUserCommand(TestCase): def test_create_simple_user(self): buf = StringIO() - call_command("create_api_user", "--email", "foo@example.com", stdout=buf) + call_command("create-api-user", "--email", "foo@example.com", stdout=buf) output = buf.getvalue() User = get_user_model() user = User.objects.get(username="foo@example.com") @@ -29,15 +29,15 @@ def test_create_simple_user(self): assert f"User foo@example.com created with API key: {user.auth_token.key}" in output def test_create_simple_user_cannot_create_user_twice_with_same_email(self): - call_command("create_api_user", "--email", "foo1@example.com") + call_command("create-api-user", "--email", "foo1@example.com") - with pytest.raises(CommandError): - call_command("create_api_user", "--email", "foo1@example.com") + with pytest.raises(CommandError) as cm: + call_command("create-api-user", "--email", "foo1@example.com") def test_create_user_with_names(self): buf = StringIO() call_command( - "create_api_user", + "create-api-user", "--email", "foo3@example.com", "--first-name", @@ -52,7 +52,3 @@ def test_create_user_with_names(self): assert user.auth_token.key assert user.first_name == "Bjorn" assert user.last_name == "Borg" - - def test_create_simple_user_demands_a_valid_email(self): - with pytest.raises(CommandError): - call_command("create_api_user", "--email", "fooNOT AN EMAIL.com") From 3b5dd775be2e892e0850d37e4310b8c2b8c2fca5 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Thu, 27 Oct 2022 16:37:55 +0200 Subject: [PATCH 06/27] Add simplified admin to create API users * Create a new ApiUser proxy model to create a minimal admin. * Streamline code and validate that a username is a valid email. * Update the management command accordingly to share common code Signed-off-by: Philippe Ombredanne --- .../management/commands/create-api-user.py | 70 +++++-------------- vulnerabilities/models.py | 4 +- .../tests/test_create_api_user_command.py | 6 +- 3 files changed, 25 insertions(+), 55 deletions(-) diff --git a/vulnerabilities/management/commands/create-api-user.py b/vulnerabilities/management/commands/create-api-user.py index e4227a93c..db471cac4 100644 --- a/vulnerabilities/management/commands/create-api-user.py +++ b/vulnerabilities/management/commands/create-api-user.py @@ -7,14 +7,12 @@ # See https://aboutcode.org for more information about nexB OSS projects. # -import getpass - -from django.contrib.auth import get_user_model -from django.contrib.auth.password_validation import validate_password from django.core import exceptions from django.core.management.base import BaseCommand from django.core.management.base import CommandError -from rest_framework.authtoken.models import Token +from django.core.validators import validate_email + +from vulnerabilities.models import ApiUser """ Create a basic API-only user based on an email. @@ -25,13 +23,11 @@ class Command(BaseCommand): help = "Create a basic passwordless user with an API key for sole API authentication usage." requires_migrations_checks = True - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.UserModel = get_user_model() - self.username_field = self.UserModel._meta.get_field(self.UserModel.USERNAME_FIELD) - def add_arguments(self, parser): - parser.add_argument("--email", help="Specifies the email for the user.") + parser.add_argument( + "--email", + help="Specifies the email for the user.", + ) parser.add_argument( "--first-name", default="", @@ -44,47 +40,17 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): - email = options["email"] - email = self.UserModel._default_manager.normalize_email(email) - username = email - - error_msg = self._validate_username(username) - if error_msg: - raise CommandError(error_msg) - - first_name = options["first_name"] or "" - last_name = options["last_name"] or "" - - password = None - user = self.UserModel._default_manager.create_user( - username=username, - email=email, - password=password, - first_name=first_name, - last_name=last_name, - ) - # this esnure that this is not a valid password - user.set_unusable_password() - user.save() - - token, _ = Token._default_manager.get_or_create(user=user) - - msg = f"User {username} created with API key: {token.key}" - self.stdout.write(msg, self.style.SUCCESS) - - def _validate_username(self, username): - """ - Validate username. If invalid, return a string error message. - """ - if self.username_field.unique: - try: - self.UserModel._default_manager.get_by_natural_key(username) - except self.UserModel.DoesNotExist: - pass - else: - return "Error: That email username is already taken." + email = options["email"] try: - self.username_field.clean(username, None) + validate_email(email) + user = ApiUser.objects.create_api_user( + username=email, + first_name=options["first_name"] or "", + last_name=options["last_name"] or "", + ) except exceptions.ValidationError as e: - return "; ".join(e.messages) + raise CommandError(str(e)) + + msg = f"User {user.email} created with API key: {user.auth_token.key}" + self.stdout.write(msg, self.style.SUCCESS) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 29c202139..cbcea2051 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -24,8 +24,8 @@ from django.db.models import Q from django.db.models.functions import Length from django.db.models.functions import Trim -ffrom django.dispatch import receiver -rom django.urls import reverse +from django.dispatch import receiver +from django.urls import reverse from packageurl import PackageURL from packageurl.contrib.django.models import PackageURLMixin from packageurl.contrib.django.models import PackageURLQuerySet diff --git a/vulnerabilities/tests/test_create_api_user_command.py b/vulnerabilities/tests/test_create_api_user_command.py index cd78a699b..d16f542dc 100644 --- a/vulnerabilities/tests/test_create_api_user_command.py +++ b/vulnerabilities/tests/test_create_api_user_command.py @@ -31,7 +31,7 @@ def test_create_simple_user(self): def test_create_simple_user_cannot_create_user_twice_with_same_email(self): call_command("create-api-user", "--email", "foo1@example.com") - with pytest.raises(CommandError) as cm: + with pytest.raises(CommandError): call_command("create-api-user", "--email", "foo1@example.com") def test_create_user_with_names(self): @@ -52,3 +52,7 @@ def test_create_user_with_names(self): assert user.auth_token.key assert user.first_name == "Bjorn" assert user.last_name == "Borg" + + def test_create_simple_user_demands_a_valid_email(self): + with pytest.raises(CommandError): + call_command("create-api-user", "--email", "fooNOT AN EMAIL.com") From 0d55cdf1647009a4eceb5a82a78d9d0146f038bb Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Thu, 27 Oct 2022 18:01:04 +0200 Subject: [PATCH 07/27] Towards API request self service Signed-off-by: Philippe Ombredanne --- vulnerabilities/forms.py | 10 +-- .../templates/api_user_creation_form.html | 23 ++---- vulnerabilities/views.py | 41 ++++------ vulnerablecode/urls.py | 74 ++++--------------- 4 files changed, 35 insertions(+), 113 deletions(-) diff --git a/vulnerabilities/forms.py b/vulnerabilities/forms.py index 566d94543..e4c01e67a 100644 --- a/vulnerabilities/forms.py +++ b/vulnerabilities/forms.py @@ -41,24 +41,20 @@ class ApiUserCreationForm(forms.ModelForm): class Meta: model = ApiUser fields = ( - "email", + "username", "first_name", "last_name", ) - def __init__(self, *args, **kwargs): - super(ApiUserCreationForm, self).__init__(*args, **kwargs) - self.fields["email"].required = True - def save(self, commit=True): return ApiUser.objects.create_api_user( - username=self.cleaned_data["email"], + username=self.cleaned_data["username"], first_name=self.cleaned_data["first_name"], last_name=self.cleaned_data["last_name"], ) def clean_username(self): - username = self.cleaned_data["email"] + username = self.cleaned_data["username"] validate_email(username) return username diff --git a/vulnerabilities/templates/api_user_creation_form.html b/vulnerabilities/templates/api_user_creation_form.html index acd473016..a590ce054 100644 --- a/vulnerabilities/templates/api_user_creation_form.html +++ b/vulnerabilities/templates/api_user_creation_form.html @@ -1,25 +1,12 @@ {% extends "base.html" %} -{%load crispy_forms_tags %} +{% block title %} +VulnerableCode API key request +{% endblock %} {% block content %} + {% include "navbar.html" %}
- {% for message in messages %} -
-
- {{ message|linebreaksbr }} -
-
- {% endfor %} - {% block title %} - VulnerableCode API key request - {% endblock %} -
-
- {% csrf_token %} - {{form|crispy }} -
- -
+ {{ form.as_p }}
{% endblock %} diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 6c85e5faa..e5e5d2bc6 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -7,13 +7,10 @@ # See https://aboutcode.org for more information about nexB OSS projects. # -from django.contrib import messages -from django.core.exceptions import ValidationError -from django.core.mail import send_mail from django.db.models import Count from django.db.models import Q from django.http.response import Http404 -from django.shortcuts import redirect +from django.http.response import HttpResponseNotAllowed from django.shortcuts import render from django.urls import reverse_lazy from django.views import View @@ -26,7 +23,6 @@ from vulnerabilities.forms import ApiUserCreationForm from vulnerabilities.forms import PackageSearchForm from vulnerabilities.forms import VulnerabilitySearchForm -from vulnerablecode.settings import env PAGE_SIZE = 20 @@ -62,7 +58,7 @@ def get_queryset(self, query=None): qs = qs.filter(Q(name__icontains=query) | Q(namespace__icontains=query)) else: # this looks like a purl: check if it quacks like a purl - purl_type = namespace = name = version = None + purl_type = namespace = name = version = qualifiers = subpath = None _, _scheme, remainder = query.partition("pkg:") remainder = remainder.strip() @@ -72,7 +68,7 @@ def get_queryset(self, query=None): try: # First, treat the query as a syntactically-correct purl purl = PackageURL.from_string(query) - purl_type, namespace, name, version, _quals, _subp = purl.to_dict().values() + purl_type, namespace, name, version, qualifiers, subpath = purl.to_dict().values() except ValueError: # Otherwise, attempt a more lenient parsing of a possibly partial purl if "/" in remainder: @@ -231,32 +227,21 @@ def get(self, request): return render(request=request, template_name=self.template_name, context=context) +def schema_view(request): + if request.method != "GET": + return HttpResponseNotAllowed() + return render(request=request, template_name="api_doc.html") + + class ApiUserCreateView(generic.CreateView): model = models.ApiUser form_class = ApiUserCreationForm template_name = "api_user_creation_form.html" def form_valid(self, form): - - try: - response = super().form_valid(form) - except ValidationError as e: - messages.error(self.request, "Email is invalid or already taken") - return redirect(self.get_success_url()) - - send_mail( - subject="VulnerableCode.io API key token", - message=f"Here is your VulnerableCode.io API key token: {self.object.auth_token}", - from_email=env.str("FROM_EMAIL", default=""), - recipient_list=[self.object.email], - fail_silently=True, - ) - - messages.success( - self.request, f"API key token sent to your email address {self.object.email}." - ) - - return response + # TODO: send an email with the API key + response = super().form_valid(form) + # TODO: return http response with a simple success message that def get_success_url(self): - return reverse_lazy("api_user_request") + return reverse_lazy("api_user_creation_success", kwargs={"uuid": self.object.pk}) diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index bd0a19e15..48511abb1 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -11,9 +11,6 @@ from django.urls import include from django.urls import path from django.urls import re_path -from django.views.generic import TemplateView -from drf_spectacular.views import SpectacularAPIView -from drf_spectacular.views import SpectacularSwaggerView from rest_framework.routers import DefaultRouter from vulnerabilities.api import AliasViewSet @@ -38,73 +35,30 @@ def __init__(self, *args, **kwargs): api_router = OptionalSlashRouter() -api_router.register("packages", PackageViewSet) +api_router.register(r"packages", PackageViewSet) # `DefaultRouter` requires `basename` when registering viewsets that don't define a queryset. -api_router.register("vulnerabilities", VulnerabilityViewSet, basename="vulnerability") -api_router.register("cpes", CPEViewSet, basename="cpe") -api_router.register("aliases", AliasViewSet, basename="alias") +api_router.register(r"vulnerabilities", VulnerabilityViewSet, basename="vulnerability") +api_router.register(r"cpes", CPEViewSet, basename="cpe") +api_router.register(r"alias", AliasViewSet, basename="alias") urlpatterns = [ - path( - "", - HomePage.as_view(), - name="home", - ), - path( - "packages/search", - PackageSearch.as_view(), - name="package_search", - ), - re_path( - r"^packages/(?Ppkg:.+)$", - PackageDetails.as_view(), - name="package_details", - ), - path( - "vulnerabilities/search", - VulnerabilitySearch.as_view(), - name="vulnerability_search", - ), + path("", HomePage.as_view(), name="home"), + path("packages/search", PackageSearch.as_view(), name="package_search"), + re_path("^packages/(?Ppkg:.+)$", PackageDetails.as_view(), name="package_details"), + path("vulnerabilities/search", VulnerabilitySearch.as_view(), name="vulnerability_search"), path( "vulnerabilities/", VulnerabilityDetails.as_view(), name="vulnerability_details", ), - path( - "api/", - include(api_router.urls), - name="api", - ), - path( - "api/schema/", - SpectacularAPIView.as_view(), - name="schema", - ), - path( - "api/docs/", - SpectacularSwaggerView.as_view(url_name="schema"), - name="api_docs", - ), - path( - "account/request_api_key/", - ApiUserCreateView.as_view(), - name="api_user_request", - ), - path( - "tos/", - TemplateView.as_view(template_name="tos.html"), - name="api_tos", - ), - path( - "admin/", - admin.site.urls, - ), + path("api/docs", schema_view, name="redoc"), + path(r"api/", include(api_router.urls)), + path("admin/", admin.site.urls), + path("user/request_api_key", ApiUserCreateView.as_view(), name="api_user_request"), ] + if DEBUG_TOOLBAR: urlpatterns += [ - path( - "__debug__/", - include("debug_toolbar.urls"), - ), + path("__debug__/", include("debug_toolbar.urls")), ] From 7bc453fffd38f89e52bd0de467272eeb94a517ff Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 11:56:43 +0200 Subject: [PATCH 08/27] Move tos to the template dir for proper styling Signed-off-by: Philippe Ombredanne --- vulnerabilities/templates/tos.html | 344 +++++++++++------------------ 1 file changed, 135 insertions(+), 209 deletions(-) diff --git a/vulnerabilities/templates/tos.html b/vulnerabilities/templates/tos.html index 6d0039d9d..252a14f28 100644 --- a/vulnerabilities/templates/tos.html +++ b/vulnerabilities/templates/tos.html @@ -1,209 +1,135 @@ -{% extends "base.html" %} - -{% block title %} -VulnerableCode.io Terms of Service -{% endblock %} - -{% block content %} -
- -

Terms of Service

-
-

VulnerableCode.io provides a data service that allows users to access - information about security vulnerabilities via the web site or API. - These Terms of Service govern your access to and use of - - https://public.vulnerablecode.io (our "Site") and our products and - services (collectively, the "Service"), and any information appearing on - the Service. Any reference to "you" or "your" means you as a user of the - Service, any reference to "we", "us", "our" or "nexB" is to nexB Inc., a - California USA corporation.

-
- -
-

By using the Service you agree and consent to these Terms of Service - including terms that limit our liability or affect your legal rights, - any referenced and incorporated guidelines and policies, including our - Code of Conduct, the terms of our Privacy Policy, and any additional - terms specific to your particular use of the Service which become part - of your agreement with us (collectively, the "Terms"). If you are using - the Service on behalf of a business, you represent to us that you have - authority to bind that business or entity to these Terms, and that - business accepts these Terms.

- -

The data provided by or obtained from the Service is licensed under - the Creative Commons Attribution-ShareAlike 4.0 International Public - License - - http://creativecommons.org/licenses/by-sa/4.0/. If you share the - data with any third party you must include the following attribution - notice:

- -
- -
-

VulnerableCode data by nexB Inc. and others is - licensed under the Creative Commons Attribution-ShareAlike 4.0 International - Public License. Visit - https://public.vulnerablecode.io for documentation and support.

-
- - -

Restrictions on Your Use of the Service

- -
- You agree that you may not: -
    -
  • - Use the Service in any way that breaches any applicable local, - national, or international law or regulation. -
  • -
  • - Use the Service in any way which could infringe the rights or - interests of nexB or any third party. -
  • -
  • - Use the Service for any illegal activity or output, or in any - way that exposes nexB, you, or other users of the Service to - harm or liability. -
  • -
  • - Engage in any activity that could damage, overload, harm or - impede the normal functioning of the Service. -
  • -
  • - Gain unauthorized access to our Site, the server on which our - Site is stored or any server, computer or database connected to - our Site. -
  • -
  • - Attack, or attempt to attack our Site via a denial-of-service - attack or a distributed denial-of service attack. -
  • -
  • - Allow, enable or assist any other person or entity to violate - any provisions of these Terms. -
  • -
-
- -

Registration

- -
-

The Service may be used only by persons who are at least of the age - of majority and can form legally binding contracts under applicable law. - The Service may not be used by persons in jurisdictions where access to - or use of it may be illegal or prohibited.

- -

The Service offers certain functionality that may require the - creation of a personal account (e.g. to use the API). You promise to - provide us with accurate, complete and updated registration information. - You are exclusively responsible for generating unique and complex - credentials, safeguarding them, and for any activities or actions taken - on the Service using such credentials. You may not transfer your account - to anyone else.

- -

We do not collect, share nor sell personal data. Data handling is - minimized to legal, regulatory, and technical requirements. We are not a - data processor. Our Privacy Policy is at https://nexb.com/privacy/.

-
- -

Changes in Service

- -
-

The Service provided by nexB is constantly evolving, and the form and - nature of the Service that nexB provides may change from time to time - without prior notice to you. Any changes to the Service, including the - release of new features, are subject to the Terms then in effect. In - addition, we may stop (permanently or temporarily) providing the Service - (or any features within the Service) without providing prior notice. We - also retain the right to create and apply limits on your use of the - Service including API access, at our sole discretion, at any time - without prior notice to you.

-
- -

Disclaimers

- -
-

Your access to and use of the service is at your own risk. you - understand and agree that the Service is provided to you on an "as is" - basis, without any representations, warranties or conditions of any - kind, whether express or implied, and including without limitation - implied representations, warranties or conditions of title, non- - infringement, merchantability, fitness for a particular purpose, - performance, durability, availability, timeliness, accuracy, or - completeness, all of which are hereby disclaimed by nexB.

- -

We do not warrant or guarantee that the Service is accurate, reliable - or correct; that the Service will meet your requirements; that the - Service will be available at any particular time or location, - uninterrupted, error-free, without defect or secure; or that any defects - or errors will be corrected.

- -

Our Service may contain links to third-party websites or other - resources. ("Third Party Content"). nexB does not control the Third - Party Content and is not responsible for the Third Party Content, - including the accuracy, availability, completeness, reliability, - security, substance or timeliness of the Third Party Content. To the - extent that the Service makes available any Third Party Content that is - made available under an open source license, you are responsible for - ensuring that you comply with all such license terms if you use such - Third Party Content. Your use of the Third Party Content is at your own - risk and subject to the terms and conditions published by the owners of - the Third Party Content.

-
- -

Limitation of Liability

- -
-

To the maximum extent permitted under applicable law, the Service is - provided on an "as is" and "as available" basis, without any - representations, warranties or conditions of any kind, whether express - or implied, and including without limitation implied representations, - warranties or conditions of title, non-infringement, merchantability, - fitness for a particular purpose, performance, durability, availability, - timeliness, accuracy, or completeness, all of which are hereby - disclaimed by nexB.

- -

To the fullest extent permitted by applicable law, (a) nexB will not - be liable to you or any third party for any indirect, incidental, - consequential, special, exemplary or punitive damages of any kind, under - any contract, tort (including negligence), strict liability or other - theory, including damages for loss of profits, use or data, loss of - other intangibles, even if advised in advance of the possibility of such - damages or losses; (b) without limiting the foregoing, nexB will not be - liable to you or any third party for damages of any kind resulting from - your use of or inability to use the Service; and (c) your sole and - exclusive remedy for dissatisfaction with the Service is to stop using - the Service.

-
- -

About These Terms

- -
-

Please note that we may update and amend these Terms from time to - time and any changes will be posted on the Site. By continuing to access - the Service after any changes become effective, you agree to be bound by - the revised Terms.

- -

The failure of nexB to enforce any right or provision of these Terms - will not be deemed a waiver of such right or provision. In the event - that any provision of these Terms is held to be invalid or - unenforceable, the remaining provisions of these Terms will remain in - full force and effect.

- -

These Terms will be governed by laws of the State of California, - without respect to its conflict of laws principles. The sole - jurisdiction and venue for any claim arising from these Terms shall be - the state and federal courts located in Santa Clara County, California, - USA.

- -

If you have any doubts as to whether your use of the Service complies - with these Terms or have a concern with any aspect of the Site or the - Service, please contact us.

- -
- -
-{% endblock %} + + + + + Terms of Service + + + + + + +
+ +

Terms of Service

+ +

Welcome

+ +

VulnerableCode.io provides a data service that allows users to access information about security vulnerabilities via the web site or API. These Terms of Service govern your access to and use of https://public.vulnerablecode.io (our “Site”) and our products and services (collectively, the "Service"), and any information appearing on the Service. Any reference to “you” or “your” means you as a user of the Service, any reference to “we”, “us”, “our” or “nexB” is to nexB Inc., a California USA corporation.

+ +

By using the Service you agree and consent to these Terms of Service including terms that limit our liability or affect your legal rights, any referenced and incorporated guidelines and policies, including our Code of Conduct, the terms of our Privacy Policy, and any additional terms specific to your particular use of the Service which become part of your agreement with us (collectively, the “Terms”). If you are using the Service on behalf of a business, you represent to us that you have authority to bind that business or entity to these Terms, and that business accepts these Terms.

+ +

The data provided by or obtained from the Service is licensed under the Creative Commons Attribution-ShareAlike 4.0 International Public License - http://creativecommons.org/licenses/by-sa/4.0/. If you share the data with any third party you must include the following attribution notice:

+ +

VulnerableCode data by nexB Inc. is licensed under the Creative Commons Attribution-ShareAlike 4.0 International Public License.

+ +

Restrictions on Your Use of the Service

+ + You agree that you may not: +
    +
  • + Use the Service in any way that breaches any applicable local, national, or international law or regulation. +
  • +
  • + Use the Service in any way which could infringe the rights or interests of nexB or any third party. +
  • +
  • + Use the Service for any illegal activity or output, or in any way that exposes nexB, you, or other users of the Service to harm or liability. +
  • +
  • + Engage in any activity that could damage, overload, harm or impede the normal functioning of the Service. +
  • +
  • + Gain unauthorized access to our Site, the server on which our Site is stored or any server, computer or database connected to our Site. +
  • +
  • + Attack, or attempt to attack our Site via a denial-of-service attack or a distributed denial-of service attack. +
  • +
  • + Allow, enable or assist any other person or entity to violate any provisions of these Terms. +
  • +
+ +

Registration

+ +

The Service may be used only by persons who are at least of the age of majority and can form legally binding contracts under applicable law. The Service may not be used by persons in jurisdictions where access to or use of it may be illegal or prohibited.

+ +

The Service offers certain functionality that may require the creation of a personal account (e.g. to use the API). You promise to provide us with accurate, complete and updated registration information. You are exclusively responsible for generating unique and complex credentials, safeguarding them, and for any activities or actions taken on the Service using such credentials. You may not transfer your account to anyone else.

+ +

We do not collect, share nor sell personal data. Data handling is minimized to legal, regulatory, and technical requirements. We are not a data processor. Our Privacy Policy is at https://nexb.com/privacy/.

+ +

Changes in Service

+ +

The Service provided by nexB is constantly evolving, and the form and nature of the Service that nexB provides may change from time to time without prior notice to you. Any changes to the Service, including the release of new features, are subject to the Terms then in effect. In addition, we may stop (permanently or temporarily) providing the Service (or any features within the Service) without providing prior notice. We also retain the right to create and apply limits on your use of the Service including API access, at our sole discretion, at any time without prior notice to you.

+ +

Disclaimers

+ +

Your access to and use of the service is at your own risk. you understand and agree that the Service is provided to you on an "as is" basis, without any representations, warranties or conditions of any kind, whether express or implied, and including without limitation implied representations, warranties or conditions of title, non-infringement, merchantability, fitness for a particular purpose, performance, durability, availability, timeliness, accuracy, or completeness, all of which are hereby disclaimed by nexB.

+ +

We do not warrant or guarantee that the Service is accurate, reliable or correct; that the Service will meet your requirements; that the Service will be available at any particular time or location, uninterrupted, error-free, without defect or secure; or that any defects or errors will be corrected.

+ +

Our Service may contain links to third-party websites or other resources. (“Third Party Content”). nexB does not control the Third Party Content and is not responsible for the Third Party Content, including the accuracy, availability, completeness, reliability, security, substance or timeliness of the Third Party Content. To the extent that the Service makes available any Third Party Content that is made available under an open source license, you are responsible for ensuring that you comply with all such license terms if you use such Third Party Content. Your use of the Third Party Content is at your own risk and subject to the terms and conditions published by the owners of the Third Party Content.

+ +

Limitation of Liability

+ +

To the maximum extent permitted under applicable law, the Service is provided on an “as is” and “as available” basis, without any representations, warranties or conditions of any kind, whether express or implied, and including without limitation implied representations, warranties or conditions of title, non-infringement, merchantability, fitness for a particular purpose, performance, durability, availability, timeliness, accuracy, or completeness, all of which are hereby disclaimed by nexB.

+ +

To the fullest extent permitted by applicable law, (a) nexB will not be liable to you or any third party for any indirect, incidental, consequential, special, exemplary or punitive damages of any kind, under any contract, tort (including negligence), strict liability or other theory, including damages for loss of profits, use or data, loss of other intangibles, even if advised in advance of the possibility of such damages or losses; (b) without limiting the foregoing, nexB will not be liable to you or any third party for damages of any kind resulting from your use of or inability to use the Service; and (c) your sole and exclusive remedy for dissatisfaction with the Service is to stop using the Service.

+ +

About These Terms

+ +

Please note that we may update and amend these Terms from time to time and any changes will be posted on the Site. By continuing to access the Service after any changes become effective, you agree to be bound by the revised Terms.

+ +

The failure of nexB to enforce any right or provision of these Terms will not be deemed a waiver of such right or provision. In the event that any provision of these Terms is held to be invalid or unenforceable, the remaining provisions of these Terms will remain in full force and effect.

+ +

These Terms will be governed by laws of the State of California, without respect to its conflict of laws principles. The sole jurisdiction and venue for any claim arising from these Terms shall be the state and federal courts located in Santa Clara County, California, USA.

+ +

If you have any doubts as to whether your use of the Service complies with these Terms or have a concern with any aspect of the Site or the Service, please contact us.

+ +
+ + + \ No newline at end of file From dbaa8f263fe9eee0887882ea16f773bf63d2a62d Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 11:57:45 +0200 Subject: [PATCH 09/27] Transform tos in a template * Use shared footer * Move navbar to base template to avoid duplication Signed-off-by: Philippe Ombredanne --- .../templates/api_user_creation_form.html | 1 - vulnerabilities/templates/tos.html | 344 +++++++++++------- 2 files changed, 209 insertions(+), 136 deletions(-) diff --git a/vulnerabilities/templates/api_user_creation_form.html b/vulnerabilities/templates/api_user_creation_form.html index a590ce054..a2ad55182 100644 --- a/vulnerabilities/templates/api_user_creation_form.html +++ b/vulnerabilities/templates/api_user_creation_form.html @@ -5,7 +5,6 @@ {% endblock %} {% block content %} - {% include "navbar.html" %}
{{ form.as_p }}
diff --git a/vulnerabilities/templates/tos.html b/vulnerabilities/templates/tos.html index 252a14f28..6d0039d9d 100644 --- a/vulnerabilities/templates/tos.html +++ b/vulnerabilities/templates/tos.html @@ -1,135 +1,209 @@ - - - - - Terms of Service - - - - - - -
- -

Terms of Service

- -

Welcome

- -

VulnerableCode.io provides a data service that allows users to access information about security vulnerabilities via the web site or API. These Terms of Service govern your access to and use of https://public.vulnerablecode.io (our “Site”) and our products and services (collectively, the "Service"), and any information appearing on the Service. Any reference to “you” or “your” means you as a user of the Service, any reference to “we”, “us”, “our” or “nexB” is to nexB Inc., a California USA corporation.

- -

By using the Service you agree and consent to these Terms of Service including terms that limit our liability or affect your legal rights, any referenced and incorporated guidelines and policies, including our Code of Conduct, the terms of our Privacy Policy, and any additional terms specific to your particular use of the Service which become part of your agreement with us (collectively, the “Terms”). If you are using the Service on behalf of a business, you represent to us that you have authority to bind that business or entity to these Terms, and that business accepts these Terms.

- -

The data provided by or obtained from the Service is licensed under the Creative Commons Attribution-ShareAlike 4.0 International Public License - http://creativecommons.org/licenses/by-sa/4.0/. If you share the data with any third party you must include the following attribution notice:

- -

VulnerableCode data by nexB Inc. is licensed under the Creative Commons Attribution-ShareAlike 4.0 International Public License.

- -

Restrictions on Your Use of the Service

- - You agree that you may not: -
    -
  • - Use the Service in any way that breaches any applicable local, national, or international law or regulation. -
  • -
  • - Use the Service in any way which could infringe the rights or interests of nexB or any third party. -
  • -
  • - Use the Service for any illegal activity or output, or in any way that exposes nexB, you, or other users of the Service to harm or liability. -
  • -
  • - Engage in any activity that could damage, overload, harm or impede the normal functioning of the Service. -
  • -
  • - Gain unauthorized access to our Site, the server on which our Site is stored or any server, computer or database connected to our Site. -
  • -
  • - Attack, or attempt to attack our Site via a denial-of-service attack or a distributed denial-of service attack. -
  • -
  • - Allow, enable or assist any other person or entity to violate any provisions of these Terms. -
  • -
- -

Registration

- -

The Service may be used only by persons who are at least of the age of majority and can form legally binding contracts under applicable law. The Service may not be used by persons in jurisdictions where access to or use of it may be illegal or prohibited.

- -

The Service offers certain functionality that may require the creation of a personal account (e.g. to use the API). You promise to provide us with accurate, complete and updated registration information. You are exclusively responsible for generating unique and complex credentials, safeguarding them, and for any activities or actions taken on the Service using such credentials. You may not transfer your account to anyone else.

- -

We do not collect, share nor sell personal data. Data handling is minimized to legal, regulatory, and technical requirements. We are not a data processor. Our Privacy Policy is at https://nexb.com/privacy/.

- -

Changes in Service

- -

The Service provided by nexB is constantly evolving, and the form and nature of the Service that nexB provides may change from time to time without prior notice to you. Any changes to the Service, including the release of new features, are subject to the Terms then in effect. In addition, we may stop (permanently or temporarily) providing the Service (or any features within the Service) without providing prior notice. We also retain the right to create and apply limits on your use of the Service including API access, at our sole discretion, at any time without prior notice to you.

- -

Disclaimers

- -

Your access to and use of the service is at your own risk. you understand and agree that the Service is provided to you on an "as is" basis, without any representations, warranties or conditions of any kind, whether express or implied, and including without limitation implied representations, warranties or conditions of title, non-infringement, merchantability, fitness for a particular purpose, performance, durability, availability, timeliness, accuracy, or completeness, all of which are hereby disclaimed by nexB.

- -

We do not warrant or guarantee that the Service is accurate, reliable or correct; that the Service will meet your requirements; that the Service will be available at any particular time or location, uninterrupted, error-free, without defect or secure; or that any defects or errors will be corrected.

- -

Our Service may contain links to third-party websites or other resources. (“Third Party Content”). nexB does not control the Third Party Content and is not responsible for the Third Party Content, including the accuracy, availability, completeness, reliability, security, substance or timeliness of the Third Party Content. To the extent that the Service makes available any Third Party Content that is made available under an open source license, you are responsible for ensuring that you comply with all such license terms if you use such Third Party Content. Your use of the Third Party Content is at your own risk and subject to the terms and conditions published by the owners of the Third Party Content.

- -

Limitation of Liability

- -

To the maximum extent permitted under applicable law, the Service is provided on an “as is” and “as available” basis, without any representations, warranties or conditions of any kind, whether express or implied, and including without limitation implied representations, warranties or conditions of title, non-infringement, merchantability, fitness for a particular purpose, performance, durability, availability, timeliness, accuracy, or completeness, all of which are hereby disclaimed by nexB.

- -

To the fullest extent permitted by applicable law, (a) nexB will not be liable to you or any third party for any indirect, incidental, consequential, special, exemplary or punitive damages of any kind, under any contract, tort (including negligence), strict liability or other theory, including damages for loss of profits, use or data, loss of other intangibles, even if advised in advance of the possibility of such damages or losses; (b) without limiting the foregoing, nexB will not be liable to you or any third party for damages of any kind resulting from your use of or inability to use the Service; and (c) your sole and exclusive remedy for dissatisfaction with the Service is to stop using the Service.

- -

About These Terms

- -

Please note that we may update and amend these Terms from time to time and any changes will be posted on the Site. By continuing to access the Service after any changes become effective, you agree to be bound by the revised Terms.

- -

The failure of nexB to enforce any right or provision of these Terms will not be deemed a waiver of such right or provision. In the event that any provision of these Terms is held to be invalid or unenforceable, the remaining provisions of these Terms will remain in full force and effect.

- -

These Terms will be governed by laws of the State of California, without respect to its conflict of laws principles. The sole jurisdiction and venue for any claim arising from these Terms shall be the state and federal courts located in Santa Clara County, California, USA.

- -

If you have any doubts as to whether your use of the Service complies with these Terms or have a concern with any aspect of the Site or the Service, please contact us.

- -
- - - \ No newline at end of file +{% extends "base.html" %} + +{% block title %} +VulnerableCode.io Terms of Service +{% endblock %} + +{% block content %} +
+ +

Terms of Service

+
+

VulnerableCode.io provides a data service that allows users to access + information about security vulnerabilities via the web site or API. + These Terms of Service govern your access to and use of + + https://public.vulnerablecode.io (our "Site") and our products and + services (collectively, the "Service"), and any information appearing on + the Service. Any reference to "you" or "your" means you as a user of the + Service, any reference to "we", "us", "our" or "nexB" is to nexB Inc., a + California USA corporation.

+
+ +
+

By using the Service you agree and consent to these Terms of Service + including terms that limit our liability or affect your legal rights, + any referenced and incorporated guidelines and policies, including our + Code of Conduct, the terms of our Privacy Policy, and any additional + terms specific to your particular use of the Service which become part + of your agreement with us (collectively, the "Terms"). If you are using + the Service on behalf of a business, you represent to us that you have + authority to bind that business or entity to these Terms, and that + business accepts these Terms.

+ +

The data provided by or obtained from the Service is licensed under + the Creative Commons Attribution-ShareAlike 4.0 International Public + License - + http://creativecommons.org/licenses/by-sa/4.0/. If you share the + data with any third party you must include the following attribution + notice:

+ +
+ +
+

VulnerableCode data by nexB Inc. and others is + licensed under the Creative Commons Attribution-ShareAlike 4.0 International + Public License. Visit + https://public.vulnerablecode.io for documentation and support.

+
+ + +

Restrictions on Your Use of the Service

+ +
+ You agree that you may not: +
    +
  • + Use the Service in any way that breaches any applicable local, + national, or international law or regulation. +
  • +
  • + Use the Service in any way which could infringe the rights or + interests of nexB or any third party. +
  • +
  • + Use the Service for any illegal activity or output, or in any + way that exposes nexB, you, or other users of the Service to + harm or liability. +
  • +
  • + Engage in any activity that could damage, overload, harm or + impede the normal functioning of the Service. +
  • +
  • + Gain unauthorized access to our Site, the server on which our + Site is stored or any server, computer or database connected to + our Site. +
  • +
  • + Attack, or attempt to attack our Site via a denial-of-service + attack or a distributed denial-of service attack. +
  • +
  • + Allow, enable or assist any other person or entity to violate + any provisions of these Terms. +
  • +
+
+ +

Registration

+ +
+

The Service may be used only by persons who are at least of the age + of majority and can form legally binding contracts under applicable law. + The Service may not be used by persons in jurisdictions where access to + or use of it may be illegal or prohibited.

+ +

The Service offers certain functionality that may require the + creation of a personal account (e.g. to use the API). You promise to + provide us with accurate, complete and updated registration information. + You are exclusively responsible for generating unique and complex + credentials, safeguarding them, and for any activities or actions taken + on the Service using such credentials. You may not transfer your account + to anyone else.

+ +

We do not collect, share nor sell personal data. Data handling is + minimized to legal, regulatory, and technical requirements. We are not a + data processor. Our Privacy Policy is at https://nexb.com/privacy/.

+
+ +

Changes in Service

+ +
+

The Service provided by nexB is constantly evolving, and the form and + nature of the Service that nexB provides may change from time to time + without prior notice to you. Any changes to the Service, including the + release of new features, are subject to the Terms then in effect. In + addition, we may stop (permanently or temporarily) providing the Service + (or any features within the Service) without providing prior notice. We + also retain the right to create and apply limits on your use of the + Service including API access, at our sole discretion, at any time + without prior notice to you.

+
+ +

Disclaimers

+ +
+

Your access to and use of the service is at your own risk. you + understand and agree that the Service is provided to you on an "as is" + basis, without any representations, warranties or conditions of any + kind, whether express or implied, and including without limitation + implied representations, warranties or conditions of title, non- + infringement, merchantability, fitness for a particular purpose, + performance, durability, availability, timeliness, accuracy, or + completeness, all of which are hereby disclaimed by nexB.

+ +

We do not warrant or guarantee that the Service is accurate, reliable + or correct; that the Service will meet your requirements; that the + Service will be available at any particular time or location, + uninterrupted, error-free, without defect or secure; or that any defects + or errors will be corrected.

+ +

Our Service may contain links to third-party websites or other + resources. ("Third Party Content"). nexB does not control the Third + Party Content and is not responsible for the Third Party Content, + including the accuracy, availability, completeness, reliability, + security, substance or timeliness of the Third Party Content. To the + extent that the Service makes available any Third Party Content that is + made available under an open source license, you are responsible for + ensuring that you comply with all such license terms if you use such + Third Party Content. Your use of the Third Party Content is at your own + risk and subject to the terms and conditions published by the owners of + the Third Party Content.

+
+ +

Limitation of Liability

+ +
+

To the maximum extent permitted under applicable law, the Service is + provided on an "as is" and "as available" basis, without any + representations, warranties or conditions of any kind, whether express + or implied, and including without limitation implied representations, + warranties or conditions of title, non-infringement, merchantability, + fitness for a particular purpose, performance, durability, availability, + timeliness, accuracy, or completeness, all of which are hereby + disclaimed by nexB.

+ +

To the fullest extent permitted by applicable law, (a) nexB will not + be liable to you or any third party for any indirect, incidental, + consequential, special, exemplary or punitive damages of any kind, under + any contract, tort (including negligence), strict liability or other + theory, including damages for loss of profits, use or data, loss of + other intangibles, even if advised in advance of the possibility of such + damages or losses; (b) without limiting the foregoing, nexB will not be + liable to you or any third party for damages of any kind resulting from + your use of or inability to use the Service; and (c) your sole and + exclusive remedy for dissatisfaction with the Service is to stop using + the Service.

+
+ +

About These Terms

+ +
+

Please note that we may update and amend these Terms from time to + time and any changes will be posted on the Site. By continuing to access + the Service after any changes become effective, you agree to be bound by + the revised Terms.

+ +

The failure of nexB to enforce any right or provision of these Terms + will not be deemed a waiver of such right or provision. In the event + that any provision of these Terms is held to be invalid or + unenforceable, the remaining provisions of these Terms will remain in + full force and effect.

+ +

These Terms will be governed by laws of the State of California, + without respect to its conflict of laws principles. The sole + jurisdiction and venue for any claim arising from these Terms shall be + the state and federal courts located in Santa Clara County, California, + USA.

+ +

If you have any doubts as to whether your use of the Service complies + with these Terms or have a concern with any aspect of the Site or the + Service, please contact us.

+ +
+ +
+{% endblock %} From 121f1d06ff3cd12bcfd7de82e219c819f786b6a2 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 12:03:27 +0200 Subject: [PATCH 10/27] Add back drf-spectacular as a dependency This will help generate an Open API documentation now that we do not have CDN issues anymore with: https://github.com/tfranzel/drf-spectacular/issues/389 Referenced-by: https://github.com/nexB/vulnerablecode/issues/454 Thanks-you-to: T. Franzel @tfranzel Signed-off-by: Philippe Ombredanne --- requirements.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index b096f9c2b..b8986908d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,6 @@ decorator==5.1.1 defusedxml==0.7.1 distro==1.7.0 Django==4.0.7 -django-crispy-forms==1.10.0 django-environ==0.8.1 django-filter==21.1 django-widget-tweaks==1.4.12 @@ -66,7 +65,7 @@ platformdirs==2.5.1 pluggy==1.0.0 pprintpp==0.4.0 prompt-toolkit==3.0.29 -psycopg2-binary==2.9.3 +psycopg2==2.9.3 ptyprocess==0.7.0 pure-eval==0.2.2 py==1.11.0 @@ -117,7 +116,7 @@ dateparser==1.1.1 fetchcode==0.2.0 drf-spectacular-sidecar==2022.10.1 -drf-spectacular==0.24.2 +drf-spectacular[sidecar]==0.24.2 coreapi==2.3.3 coreschema==0.0.4 itypes==1.2.0 \ No newline at end of file From a82d34e5db6baaa13fddd4196d8f7c8e8d21d06e Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 12:12:15 +0200 Subject: [PATCH 11/27] Adopt drf_spectacular for live API doc Signed-off-by: Philippe Ombredanne --- vulnerablecode/settings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index 9de4645f0..f85c0098c 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -83,7 +83,6 @@ "drf_spectacular_sidecar", ) - MIDDLEWARE = ( "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -196,7 +195,6 @@ "bulk_search_cpes": "5/day", } - USE_L10N = True USE_TZ = True @@ -211,7 +209,6 @@ str(PROJECT_DIR / "static"), ] - CRISPY_TEMPLATE_PACK = "bootstrap4" # Third-party apps From 301b314763ef46cdb4cd8b4a8a481e9427cf5a47 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 12:17:21 +0200 Subject: [PATCH 12/27] Add doc links to generate API docs Signed-off-by: Philippe Ombredanne --- docs/source/api.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index fd991403a..fb148f194 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -4,6 +4,12 @@ API overview ======================== +Browse the Open API documentation +------------------------------------ + +- https://public.vulnerablecode.io/api/docs/ for documentation with Swagger +- https://public.vulnerablecode.io/api/schema/ for the OpenAPI schema + Enable the API key authentication ------------------------------------ @@ -26,7 +32,7 @@ This can be done in the admin and from the command line:: Access the API using curl ----------------------------- - curl -X GET -H 'Authorization: Token ' http://public.vulnerablecode.io/api/ + curl -X GET -H 'Authorization: Token ' https://public.vulnerablecode.io/api/ API endpoints @@ -37,7 +43,8 @@ There are two primary endpoints: - packages/: this is the main endpoint where you can lookup vulnerabilities by package. -- vulnerabilities.: for lookup by vulnerabilities +- vulnerabilities/: to lookup by vulnerabilities -And two secondary endpoints, used to query aliases and CPEs: cpes/ and alias/ +And two secondary endpoints, used to query vulnerability aliases (such as CVEs) +and vulnerability by CPEs: cpes/ and aliases/ From fdf99c8728ea8c22faf6570e98ab7bcb06eff1cc Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 12:18:13 +0200 Subject: [PATCH 13/27] Improve API doc and typing Signed-off-by: Philippe Ombredanne --- vulnerabilities/api.py | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index d137e3174..55f5b7590 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -23,7 +23,6 @@ from vulnerabilities.models import VulnerabilityReference from vulnerabilities.models import VulnerabilitySeverity from vulnerabilities.models import get_purl_query_lookups -from vulnerabilities.throttling import StaffUserRateThrottle class VulnerabilitySeveritySerializer(serializers.ModelSerializer): @@ -55,7 +54,7 @@ class Meta: class VulnSerializerRefsAndSummary(serializers.HyperlinkedModelSerializer): """ - Lookup vulnerabilities references by aliases (such as a CVE). + Used for nesting inside package focused APIs. """ fixed_packages = MinimalPackageSerializer( @@ -71,7 +70,7 @@ class Meta: class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer): """ - Lookup vulnerabilities by aliases (such as a CVE). + Used for nesting inside package focused APIs. """ class Meta: @@ -113,10 +112,6 @@ class Meta: class PackageSerializer(serializers.HyperlinkedModelSerializer): - """ - Lookup software package using Package URLs - """ - def to_representation(self, instance): data = super().to_representation(instance) data["unresolved_vulnerabilities"] = data["affected_by_vulnerabilities"] @@ -229,14 +224,12 @@ class PackageViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = PackageSerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = PackageFilterSet - throttle_classes = [StaffUserRateThrottle] - throttle_scope = "packages" # TODO: Fix the swagger documentation for this endpoint - @action(detail=False, methods=["post"], throttle_scope="bulk_search_packages") + @action(detail=False, methods=["post"]) def bulk_search(self, request): """ - Lookup for vulnerable packages using many Package URLs at once. + See https://github.com/nexB/vulnerablecode/pull/369#issuecomment-796877606 for docs """ response = [] purls = request.data.get("purls", []) or [] @@ -257,7 +250,7 @@ def bulk_search(self, request): if purl_data: purl_response = PackageSerializer(purl_data[0], context={"request": request}).data else: - purl_response = purl.to_dict() + purl_response = purl purl_response["unresolved_vulnerabilities"] = [] purl_response["resolved_vulnerabilities"] = [] purl_response["purl"] = purl_string @@ -265,10 +258,10 @@ def bulk_search(self, request): return Response(response) - @action(detail=False, methods=["get"], throttle_scope="vulnerable_packages") + @action(detail=False, methods=["get"]) def all(self, request): """ - Return the Package URLs of all packages known to be vulnerable. + Return all the vulnerable Package URLs. """ vulnerable_packages = Package.objects.vulnerable().only(*PackageURL._fields).distinct() vulnerable_purls = [str(package.purl) for package in vulnerable_packages] @@ -283,7 +276,7 @@ class Meta: class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet): """ - Lookup for vulnerabilities affecting packages. + Lookup for vulnerable packages by vulnerability. """ def get_fixed_packages_qs(self): @@ -317,8 +310,6 @@ def get_queryset(self): serializer_class = VulnerabilitySerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = VulnerabilityFilterSet - throttle_classes = [StaffUserRateThrottle] - throttle_scope = "vulnerabilities" class CPEFilterSet(filters.FilterSet): @@ -331,7 +322,7 @@ def filter_cpe(self, queryset, name, value): class CPEViewSet(viewsets.ReadOnlyModelViewSet): """ - Lookup for vulnerabilities by CPE (https://nvd.nist.gov/products/cpe) + Lookup for vulnerable packages by CPE. """ queryset = Vulnerability.objects.filter( @@ -339,14 +330,12 @@ class CPEViewSet(viewsets.ReadOnlyModelViewSet): ).distinct() serializer_class = VulnerabilitySerializer filter_backends = (filters.DjangoFilterBackend,) - throttle_classes = [StaffUserRateThrottle] filterset_class = CPEFilterSet - throttle_scope = "cpes" - @action(detail=False, methods=["post"], throttle_scope="bulk_search_cpes") + @action(detail=False, methods=["post"]) def bulk_search(self, request): """ - Lookup for vulnerabilities using many CPEs at once. + This endpoint is used to search for vulnerabilities by more than one CPE. """ cpes = request.data.get("cpes", []) or [] if not cpes or not isinstance(cpes, list): @@ -377,13 +366,10 @@ def filter_alias(self, queryset, name, value): class AliasViewSet(viewsets.ReadOnlyModelViewSet): """ - Lookup for vulnerabilities by vulnerability aliases such as a CVE - (https://nvd.nist.gov/general/cve-process). + Lookup for vulnerabilities by vulnerability aliases such as a CVE. """ queryset = Vulnerability.objects.all() serializer_class = VulnerabilitySerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = AliasFilterSet - throttle_classes = [StaffUserRateThrottle] - throttle_scope = "aliases" From cfbd05fe383ad4aaea9ae8e30a5d4971c776d898 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 12:19:28 +0200 Subject: [PATCH 14/27] Remove outdated schema_view from views This is no longer needed as the OpenAPI schema is available directly though drf-spectacular Signed-off-by: Philippe Ombredanne --- vulnerabilities/views.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index e5e5d2bc6..36fbdfd26 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -227,12 +227,6 @@ def get(self, request): return render(request=request, template_name=self.template_name, context=context) -def schema_view(request): - if request.method != "GET": - return HttpResponseNotAllowed() - return render(request=request, template_name="api_doc.html") - - class ApiUserCreateView(generic.CreateView): model = models.ApiUser form_class = ApiUserCreationForm From fc2858f31543b9901e1796752a035da46812fbcf Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 14:48:21 +0200 Subject: [PATCH 15/27] Generate API documentation And streamline urls Signed-off-by: Philippe Ombredanne --- vulnerabilities/tests/test_auth.py | 4 +- vulnerablecode/settings.py | 3 ++ vulnerablecode/urls.py | 75 ++++++++++++++++++++++++------ 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/vulnerabilities/tests/test_auth.py b/vulnerabilities/tests/test_auth.py index 131bafd39..8a597cbbd 100644 --- a/vulnerabilities/tests/test_auth.py +++ b/vulnerabilities/tests/test_auth.py @@ -24,7 +24,9 @@ class VulnerableCodeAuthTest(TestCase): def setUp(self): - self.basic_user = ApiUser.objects.create_api_user(username="basic_user@foo.com") + self.basic_user = ApiUser.objects.create_api_user( + username="basic_user@foo.com", password=TEST_PASSWORD + ) def test_vulnerablecode_auth_api_required_authentication(self): response = self.client.get(api_package_url) diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index f85c0098c..0a19625b0 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -320,3 +320,6 @@ INTERNAL_IPS = [ "127.0.0.1", ] + +if not VULNERABLECODEIO_REQUIRE_AUTHENTICATION: + REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.AllowAny",) diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index 48511abb1..258c09366 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -11,6 +11,9 @@ from django.urls import include from django.urls import path from django.urls import re_path +from django.views.generic import TemplateView +from drf_spectacular.views import SpectacularAPIView +from drf_spectacular.views import SpectacularSwaggerView from rest_framework.routers import DefaultRouter from vulnerabilities.api import AliasViewSet @@ -23,7 +26,6 @@ from vulnerabilities.views import PackageSearch from vulnerabilities.views import VulnerabilityDetails from vulnerabilities.views import VulnerabilitySearch -from vulnerabilities.views import schema_view from vulnerablecode.settings import DEBUG_TOOLBAR @@ -35,30 +37,73 @@ def __init__(self, *args, **kwargs): api_router = OptionalSlashRouter() -api_router.register(r"packages", PackageViewSet) +api_router.register("packages", PackageViewSet) # `DefaultRouter` requires `basename` when registering viewsets that don't define a queryset. -api_router.register(r"vulnerabilities", VulnerabilityViewSet, basename="vulnerability") -api_router.register(r"cpes", CPEViewSet, basename="cpe") -api_router.register(r"alias", AliasViewSet, basename="alias") +api_router.register("vulnerabilities", VulnerabilityViewSet, basename="vulnerability") +api_router.register("cpes", CPEViewSet, basename="cpe") +api_router.register("aliases", AliasViewSet, basename="alias") urlpatterns = [ - path("", HomePage.as_view(), name="home"), - path("packages/search", PackageSearch.as_view(), name="package_search"), - re_path("^packages/(?Ppkg:.+)$", PackageDetails.as_view(), name="package_details"), - path("vulnerabilities/search", VulnerabilitySearch.as_view(), name="vulnerability_search"), + path( + "", + HomePage.as_view(), + name="home", + ), + path( + "packages/search", + PackageSearch.as_view(), + name="package_search", + ), + re_path( + r"^packages/(?Ppkg:.+)$", + PackageDetails.as_view(), + name="package_details", + ), + path( + "vulnerabilities/search", + VulnerabilitySearch.as_view(), + name="vulnerability_search", + ), path( "vulnerabilities/", VulnerabilityDetails.as_view(), name="vulnerability_details", ), - path("api/docs", schema_view, name="redoc"), - path(r"api/", include(api_router.urls)), - path("admin/", admin.site.urls), - path("user/request_api_key", ApiUserCreateView.as_view(), name="api_user_request"), + path( + "api", + include(api_router.urls), + name="api", + ), + path( + "api/schema", + SpectacularAPIView.as_view(), + name="schema", + ), + path( + "api/docs", + SpectacularSwaggerView.as_view(url_name="schema"), + name="api_docs", + ), + path( + "api/request_api_key", + ApiUserCreateView.as_view(), + name="api_user_request", + ), + path( + "tos", + TemplateView.as_view(template_name="tos.html"), + name="api_tos", + ), + path( + "admin/", + admin.site.urls, + ), ] - if DEBUG_TOOLBAR: urlpatterns += [ - path("__debug__/", include("debug_toolbar.urls")), + path( + "__debug__/", + include("debug_toolbar.urls"), + ), ] From 06aafa6d8ce9b78bf21b3d3c5d590dcc1dc839e7 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 16:07:11 +0200 Subject: [PATCH 16/27] Remove unused variables and imports Signed-off-by: Philippe Ombredanne --- vulnerabilities/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 36fbdfd26..f2a725f31 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -10,7 +10,6 @@ from django.db.models import Count from django.db.models import Q from django.http.response import Http404 -from django.http.response import HttpResponseNotAllowed from django.shortcuts import render from django.urls import reverse_lazy from django.views import View @@ -58,7 +57,7 @@ def get_queryset(self, query=None): qs = qs.filter(Q(name__icontains=query) | Q(namespace__icontains=query)) else: # this looks like a purl: check if it quacks like a purl - purl_type = namespace = name = version = qualifiers = subpath = None + purl_type = namespace = name = version = None _, _scheme, remainder = query.partition("pkg:") remainder = remainder.strip() @@ -68,7 +67,7 @@ def get_queryset(self, query=None): try: # First, treat the query as a syntactically-correct purl purl = PackageURL.from_string(query) - purl_type, namespace, name, version, qualifiers, subpath = purl.to_dict().values() + purl_type, namespace, name, version, _quals, _subp = purl.to_dict().values() except ValueError: # Otherwise, attempt a more lenient parsing of a possibly partial purl if "/" in remainder: From b2dbdbd71007ade6212c85670e5c85b9c24d06d2 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 16:12:11 +0200 Subject: [PATCH 17/27] Improve API documentation Override the swagger UI template Format and improve settings and ruls.py Signed-off-by: Philippe Ombredanne --- vulnerabilities/api.py | 21 +++++++++++++-------- vulnerablecode/settings.py | 3 --- vulnerablecode/urls.py | 10 +++++----- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index 55f5b7590..037af1910 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -54,7 +54,7 @@ class Meta: class VulnSerializerRefsAndSummary(serializers.HyperlinkedModelSerializer): """ - Used for nesting inside package focused APIs. + Lookup vulnerabilities references by aliases (such as a CVE). """ fixed_packages = MinimalPackageSerializer( @@ -70,7 +70,7 @@ class Meta: class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer): """ - Used for nesting inside package focused APIs. + Lookup vulnerabilities by aliases (such as a CVE). """ class Meta: @@ -112,6 +112,10 @@ class Meta: class PackageSerializer(serializers.HyperlinkedModelSerializer): + """ + Lookup software package using Package URLs + """ + def to_representation(self, instance): data = super().to_representation(instance) data["unresolved_vulnerabilities"] = data["affected_by_vulnerabilities"] @@ -229,7 +233,7 @@ class PackageViewSet(viewsets.ReadOnlyModelViewSet): @action(detail=False, methods=["post"]) def bulk_search(self, request): """ - See https://github.com/nexB/vulnerablecode/pull/369#issuecomment-796877606 for docs + Lookup for vulnerable packages using many Package URLs at once. """ response = [] purls = request.data.get("purls", []) or [] @@ -261,7 +265,7 @@ def bulk_search(self, request): @action(detail=False, methods=["get"]) def all(self, request): """ - Return all the vulnerable Package URLs. + Return the Package URLs of all packages known to be vulnerable. """ vulnerable_packages = Package.objects.vulnerable().only(*PackageURL._fields).distinct() vulnerable_purls = [str(package.purl) for package in vulnerable_packages] @@ -276,7 +280,7 @@ class Meta: class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet): """ - Lookup for vulnerable packages by vulnerability. + Lookup for vulnerabilities affecting packages. """ def get_fixed_packages_qs(self): @@ -322,7 +326,7 @@ def filter_cpe(self, queryset, name, value): class CPEViewSet(viewsets.ReadOnlyModelViewSet): """ - Lookup for vulnerable packages by CPE. + Lookup for vulnerabilities by CPE (https://nvd.nist.gov/products/cpe) """ queryset = Vulnerability.objects.filter( @@ -335,7 +339,7 @@ class CPEViewSet(viewsets.ReadOnlyModelViewSet): @action(detail=False, methods=["post"]) def bulk_search(self, request): """ - This endpoint is used to search for vulnerabilities by more than one CPE. + Lookup for vulnerabilities using many CPEs at once. """ cpes = request.data.get("cpes", []) or [] if not cpes or not isinstance(cpes, list): @@ -366,7 +370,8 @@ def filter_alias(self, queryset, name, value): class AliasViewSet(viewsets.ReadOnlyModelViewSet): """ - Lookup for vulnerabilities by vulnerability aliases such as a CVE. + Lookup for vulnerabilities by vulnerability aliases such as a CVE + (https://nvd.nist.gov/general/cve-process). """ queryset = Vulnerability.objects.all() diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index 0a19625b0..f85c0098c 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -320,6 +320,3 @@ INTERNAL_IPS = [ "127.0.0.1", ] - -if not VULNERABLECODEIO_REQUIRE_AUTHENTICATION: - REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.AllowAny",) diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index 258c09366..a35f5c22b 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -70,27 +70,27 @@ def __init__(self, *args, **kwargs): name="vulnerability_details", ), path( - "api", + "api/", include(api_router.urls), name="api", ), path( - "api/schema", + "api/schema/", SpectacularAPIView.as_view(), name="schema", ), path( - "api/docs", + "api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="api_docs", ), path( - "api/request_api_key", + "account/request_api_key/", ApiUserCreateView.as_view(), name="api_user_request", ), path( - "tos", + "tos/", TemplateView.as_view(template_name="tos.html"), name="api_tos", ), From 1af762565ee9f1689973c2fe913ef853eec52438 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 16:18:28 +0200 Subject: [PATCH 18/27] Doe not extras in pip constraints Signed-off-by: Philippe Ombredanne --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b8986908d..4e6d72a9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -116,7 +116,7 @@ dateparser==1.1.1 fetchcode==0.2.0 drf-spectacular-sidecar==2022.10.1 -drf-spectacular[sidecar]==0.24.2 +drf-spectacular==0.24.2 coreapi==2.3.3 coreschema==0.0.4 itypes==1.2.0 \ No newline at end of file From c6fe1596e001b0a8a743467c4d15f85531be4c92 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 28 Oct 2022 20:37:07 +0530 Subject: [PATCH 19/27] Configire email settings Signed-off-by: Tushar Goel --- vulnerabilities/forms.py | 4 ++++ .../templates/api_user_creation_form.html | 15 ++++++++++---- vulnerabilities/views.py | 20 +++++++++++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/vulnerabilities/forms.py b/vulnerabilities/forms.py index e4c01e67a..aff010d19 100644 --- a/vulnerabilities/forms.py +++ b/vulnerabilities/forms.py @@ -46,6 +46,10 @@ class Meta: "last_name", ) + def __init__(self, *args, **kwargs): + super(ApiUserCreationForm, self).__init__(*args, **kwargs) + self.fields["username"].help_text = f"
  • {self.fields['username'].help_text}
" + def save(self, commit=True): return ApiUser.objects.create_api_user( username=self.cleaned_data["username"], diff --git a/vulnerabilities/templates/api_user_creation_form.html b/vulnerabilities/templates/api_user_creation_form.html index a2ad55182..69f0f5489 100644 --- a/vulnerabilities/templates/api_user_creation_form.html +++ b/vulnerabilities/templates/api_user_creation_form.html @@ -1,11 +1,18 @@ {% extends "base.html" %} +{%load crispy_forms_tags %} -{% block title %} -VulnerableCode API key request -{% endblock %} {% block content %}
- {{ form.as_p }} + {% block title %} + VulnerableCode API key request + {% endblock %} +
+
+ {% csrf_token %} + {{form|crispy }} +
+ +
{% endblock %} diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index f2a725f31..c7def3f5a 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -7,8 +7,10 @@ # See https://aboutcode.org for more information about nexB OSS projects. # +from django.core.mail import send_mail from django.db.models import Count from django.db.models import Q +from django.http import HttpResponse from django.http.response import Http404 from django.shortcuts import render from django.urls import reverse_lazy @@ -22,6 +24,7 @@ from vulnerabilities.forms import ApiUserCreationForm from vulnerabilities.forms import PackageSearchForm from vulnerabilities.forms import VulnerabilitySearchForm +from vulnerablecode.settings import env PAGE_SIZE = 20 @@ -232,9 +235,18 @@ class ApiUserCreateView(generic.CreateView): template_name = "api_user_creation_form.html" def form_valid(self, form): - # TODO: send an email with the API key - response = super().form_valid(form) - # TODO: return http response with a simple success message that + super().form_valid(form) + + send_mail( + subject="VCIO API Key", + message=f"Here is your token for the VCIO API: {self.object.auth_token}", + from_email=env.str("FROM_EMAIL", default=""), + recipient_list=[self.object.email], + ) + + return HttpResponse( + f"We have mailed you the token for VCIO API on this email {self.object.email}" + ) def get_success_url(self): - return reverse_lazy("api_user_creation_success", kwargs={"uuid": self.object.pk}) + return reverse_lazy("home") From b19bd164ed3c71a67eecc904dba340af2c18565c Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 20:33:06 +0200 Subject: [PATCH 20/27] Use psycopg2-binary for consistency Signed-off-by: Philippe Ombredanne --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4e6d72a9b..b096f9c2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ decorator==5.1.1 defusedxml==0.7.1 distro==1.7.0 Django==4.0.7 +django-crispy-forms==1.10.0 django-environ==0.8.1 django-filter==21.1 django-widget-tweaks==1.4.12 @@ -65,7 +66,7 @@ platformdirs==2.5.1 pluggy==1.0.0 pprintpp==0.4.0 prompt-toolkit==3.0.29 -psycopg2==2.9.3 +psycopg2-binary==2.9.3 ptyprocess==0.7.0 pure-eval==0.2.2 py==1.11.0 From 6cd8538d310a1e19c27ee96cd16904ca7886e753 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 28 Oct 2022 20:34:45 +0200 Subject: [PATCH 21/27] Streamline API key view documentation Signed-off-by: Philippe Ombredanne --- vulnerabilities/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index c7def3f5a..d9ff4331f 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -238,14 +238,14 @@ def form_valid(self, form): super().form_valid(form) send_mail( - subject="VCIO API Key", - message=f"Here is your token for the VCIO API: {self.object.auth_token}", + subject="VulnerableCode.io API key token", + message=f"Here is your VulnerableCode.io API key token: {self.object.auth_token}", from_email=env.str("FROM_EMAIL", default=""), recipient_list=[self.object.email], ) return HttpResponse( - f"We have mailed you the token for VCIO API on this email {self.object.email}" + f"Check your email for VulnerableCode.io API key token: {self.object.email}" ) def get_success_url(self): From caaf6c085aa6591cdd899ad55b0b0dfca4120af1 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Sat, 29 Oct 2022 23:20:44 +0200 Subject: [PATCH 22/27] Rename command as create_api_user Prefer underscore to dash Signed-off-by: Philippe Ombredanne --- docs/source/api.rst | 2 +- .../management/commands/create-api-user.py | 56 ------------------- .../tests/test_create_api_user_command.py | 10 ++-- 3 files changed, 6 insertions(+), 62 deletions(-) delete mode 100644 vulnerabilities/management/commands/create-api-user.py diff --git a/docs/source/api.rst b/docs/source/api.rst index fb148f194..c779784a5 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -25,7 +25,7 @@ Create an API key-only user This can be done in the admin and from the command line:: - $ ./manage.py create-api-user --email "p4@nexb.com" --first-name="Phil" --last-name "Goel" + $ ./manage.py create_api_user --email "p4@nexb.com" --first-name="Phil" --last-name "Goel" User p4@nexb.com created with API key: ce8616b929d2adsddd6146346c2f26536423423491 diff --git a/vulnerabilities/management/commands/create-api-user.py b/vulnerabilities/management/commands/create-api-user.py deleted file mode 100644 index db471cac4..000000000 --- a/vulnerabilities/management/commands/create-api-user.py +++ /dev/null @@ -1,56 +0,0 @@ -# -# Copyright (c) nexB Inc. and others. All rights reserved. -# VulnerableCode is a trademark of nexB Inc. -# SPDX-License-Identifier: Apache-2.0 -# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. -# See https://github.com/nexB/vulnerablecode for support or download. -# See https://aboutcode.org for more information about nexB OSS projects. -# - -from django.core import exceptions -from django.core.management.base import BaseCommand -from django.core.management.base import CommandError -from django.core.validators import validate_email - -from vulnerabilities.models import ApiUser - -""" -Create a basic API-only user based on an email. -""" - - -class Command(BaseCommand): - help = "Create a basic passwordless user with an API key for sole API authentication usage." - requires_migrations_checks = True - - def add_arguments(self, parser): - parser.add_argument( - "--email", - help="Specifies the email for the user.", - ) - parser.add_argument( - "--first-name", - default="", - help="First name.", - ) - parser.add_argument( - "--last-name", - default="", - help="Last name.", - ) - - def handle(self, *args, **options): - - email = options["email"] - try: - validate_email(email) - user = ApiUser.objects.create_api_user( - username=email, - first_name=options["first_name"] or "", - last_name=options["last_name"] or "", - ) - except exceptions.ValidationError as e: - raise CommandError(str(e)) - - msg = f"User {user.email} created with API key: {user.auth_token.key}" - self.stdout.write(msg, self.style.SUCCESS) diff --git a/vulnerabilities/tests/test_create_api_user_command.py b/vulnerabilities/tests/test_create_api_user_command.py index d16f542dc..6c54fca24 100644 --- a/vulnerabilities/tests/test_create_api_user_command.py +++ b/vulnerabilities/tests/test_create_api_user_command.py @@ -19,7 +19,7 @@ class TestCreateApiUserCommand(TestCase): def test_create_simple_user(self): buf = StringIO() - call_command("create-api-user", "--email", "foo@example.com", stdout=buf) + call_command("create_api_user", "--email", "foo@example.com", stdout=buf) output = buf.getvalue() User = get_user_model() user = User.objects.get(username="foo@example.com") @@ -29,15 +29,15 @@ def test_create_simple_user(self): assert f"User foo@example.com created with API key: {user.auth_token.key}" in output def test_create_simple_user_cannot_create_user_twice_with_same_email(self): - call_command("create-api-user", "--email", "foo1@example.com") + call_command("create_api_user", "--email", "foo1@example.com") with pytest.raises(CommandError): - call_command("create-api-user", "--email", "foo1@example.com") + call_command("create_api_user", "--email", "foo1@example.com") def test_create_user_with_names(self): buf = StringIO() call_command( - "create-api-user", + "create_api_user", "--email", "foo3@example.com", "--first-name", @@ -55,4 +55,4 @@ def test_create_user_with_names(self): def test_create_simple_user_demands_a_valid_email(self): with pytest.raises(CommandError): - call_command("create-api-user", "--email", "fooNOT AN EMAIL.com") + call_command("create_api_user", "--email", "fooNOT AN EMAIL.com") From fc8362b91ccd857eed260d74f175595a9b513ec1 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Sun, 30 Oct 2022 21:40:10 +0100 Subject: [PATCH 23/27] Rename and improve cpe<->purl mapping * Use proper queryset instead of duplicated code. * Update Package and Vulnerability querysets and use these This streamlines some of the core naming and duplication issues * Refactor NVD importer core logic around a CveItem object * Use new querysets rather than refetching from the NVD * Add license and license notice * Update documentation and tests accordingly Signed-off-by: Philippe Ombredanne --- docs/source/command-line-interface.rst | 23 +- pyproject.toml | 2 +- vulnerabilities/api.py | 2 +- vulnerabilities/importers/nvd.py | 332 +++++++++++------- .../commands/create_cpe_to_purl_map.py | 97 ----- .../management/commands/purl2cpe.py | 108 ++++++ vulnerabilities/models.py | 325 +++++++++++++++-- .../templates/vulnerability_details.html | 20 +- vulnerabilities/tests/test_nvd.py | 69 ++-- vulnerabilities/views.py | 107 +----- 10 files changed, 683 insertions(+), 402 deletions(-) delete mode 100644 vulnerabilities/management/commands/create_cpe_to_purl_map.py create mode 100644 vulnerabilities/management/commands/purl2cpe.py diff --git a/docs/source/command-line-interface.rst b/docs/source/command-line-interface.rst index f4a02858d..d5afe3ac9 100644 --- a/docs/source/command-line-interface.rst +++ b/docs/source/command-line-interface.rst @@ -3,7 +3,7 @@ Command Line Interface ====================== -The main entry point is Django's :guilabel:`manage.py` management commands. +The main entry point is the Django :guilabel:`manage.py` management command script. ``$ ./manage.py --help`` ------------------------ @@ -14,9 +14,10 @@ VulnerableCode's own commands are listed under the ``[vulnerabilities]`` section $ ./manage.py --help ... [vulnerabilities] - create_cpe_to_purl_map - importer - improver + import + improve + purl2cpe + ``$ ./manage.py --help`` --------------------------------------- @@ -58,3 +59,17 @@ Other variations: * ``--list`` List all available improvers * ``--all`` Run all available improvers + + + +``$ ./manage.py purl2cpe --destination 2 and cpe_comps[2] == "h": - return True + vs = VulnerabilitySeverity( + system=severity_systems.CVSSV2_VECTOR, + value=str(cvss_v2.get("vectorString") or ""), + ) + severities.append(vs) - return False + return severities + @property + def reference_urls(self): + """ + Return a list unique of reference URLs. + """ + # FIXME: we should also collect additional data from the references such as tags and ids -def extract_cpes(cve_item): - """ - Return a list of CPEs for a given CVE item. - """ - cpes = set() - for node in get_item(cve_item, "configurations", "nodes") or []: - for cpe_data in node.get("cpe_match") or []: - cpe23_uri = cpe_data.get("cpe23Uri") - if cpe23_uri: - cpes.add(cpe23_uri) - return cpes + urls = [] + for reference in get_item(self.cve_item, "cve", "references", "reference_data") or []: + ref_url = reference.get("url") + if ref_url and ref_url.startswith(("http", "ftp")) and ref_url not in urls: + urls.append(ref_url) + return urls + @property + def references(self): + """ + Return a list of AdvisoryReference. + """ + # FIXME: we should also collect additional data from the references such as tags and ids + references = [] -def extract_severity_scores(cve_item): - """ - Yield a vulnerability severity for each `cve_item`. - """ - if not isinstance(cve_item, dict): - return None - impact = cve_item.get("impact") or {} - base_metric_v3 = impact.get("baseMetricV3") or {} - if base_metric_v3: - cvss_v3 = get_item(base_metric_v3, "cvssV3") - yield VulnerabilitySeverity( - system=severity_systems.CVSSV3, - value=str(cvss_v3.get("baseScore") or ""), - ) - yield VulnerabilitySeverity( - system=severity_systems.CVSSV3_VECTOR, - value=str(cvss_v3.get("vectorString") or ""), - ) + # we track each CPE as a reference for now + for cpe in self.cpes: + cpe_url = f"https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query={cpe}" + references.append(Reference(reference_id=cpe, url=cpe_url)) - base_metric_v2 = impact.get("baseMetricV2") or {} - if base_metric_v2: - cvss_v2 = base_metric_v2.get("cvssV2") or {} - yield VulnerabilitySeverity( - system=severity_systems.CVSSV2, - value=str(cvss_v2.get("baseScore") or ""), + # FIXME: we also add the CVE proper as a reference, but is this correct? + references.append( + Reference( + url=f"https://nvd.nist.gov/vuln/detail/{self.cve_id}", + reference_id=self.cve_id, + severities=self.severities, + ) ) - yield VulnerabilitySeverity( - system=severity_systems.CVSSV2_VECTOR, - value=str(cvss_v2.get("vectorString") or ""), + + # clean to remove dupes for the CVE id proper + ref_urls = [ + ru + for ru in self.reference_urls + if ru != f"https://nvd.nist.gov/vuln/detail/{self.cve_id}" + ] + references.extend([Reference(url=url) for url in ref_urls]) + + return references + + @property + def is_related_to_hardware(self): + """ + Return True if this CVE item is for hardware (as opposed to software). + """ + return any(is_related_to_hardware(cpe) for cpe in self.cpes) + + def to_advisory(self): + """ + Return an AdvisoryData object from this CVE item + """ + return AdvisoryData( + aliases=[self.cve_id], + summary=self.summary, + references=self.references, + date_published=dateparser.parse(self.cve_item.get("publishedDate")), ) + + +def is_related_to_hardware(cpe): + """ + Return True if the ``cpe`` is related to hardware. + """ + cpe_comps = cpe.split(":") + # CPE follow the format cpe:cpe_version:product_type:vendor:product + return len(cpe_comps) > 2 and cpe_comps[2] == "h" diff --git a/vulnerabilities/management/commands/create_cpe_to_purl_map.py b/vulnerabilities/management/commands/create_cpe_to_purl_map.py deleted file mode 100644 index 15c95800b..000000000 --- a/vulnerabilities/management/commands/create_cpe_to_purl_map.py +++ /dev/null @@ -1,97 +0,0 @@ -# -# Copyright (c) nexB Inc. and others. All rights reserved. -# VulnerableCode is a trademark of nexB Inc. -# SPDX-License-Identifier: Apache-2.0 -# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. -# See https://github.com/nexB/vulnerablecode for support or download. -# See https://aboutcode.org for more information about nexB OSS projects. -# - -import json -import os -from datetime import date -from itertools import chain - -from django.core.management.base import BaseCommand - -from vulnerabilities import models -from vulnerabilities.importers.nvd import BASE_URL as nvd_base_url -from vulnerabilities.importers.nvd import NVDImporter as nvd_utils - - -class Command(BaseCommand): - """ - This script creates a mapping of CPEs to PURLs grouped by the affecting CVE. - It does this by doing the following: - 1. Iterate over all CVEs found in VulnerableCode's db. - 2. Look for the CVE being iterated upon in the NVD. - 3. Get the list of all CPEs which are affected by this CVE from NVD entry. - 4. Get the list of all PURLs which are affected by this CVE from VulnerableCode's db. - 5. Map the list of CPEs and PURLs from #3 and #4 together. - """ - - def add_arguments(self, parser): - - parser.add_argument( - "--vulnerable_purls_only", action="store_true", help="Map only vulnerable PURLs to CPEs" - ) - - parser.add_argument( - "--patched_purls_only", action="store_true", help="Map only patching PURLs to CPEs" - ) - - @staticmethod - def get_packages(vulnerability, vulnerable_purls_only, patched_purls_only): - if vulnerable_purls_only and not patched_purls_only: - return vulnerability.vulnerable_packages.all() - - elif patched_purls_only and not vulnerable_purls_only: - return vulnerability.patched_packages.all() - - return chain(vulnerability.patched_packages.all(), vulnerability.vulnerable_packages.all()) - - def handle(self, *args, **options): - current_year = date.today().year - # NVD json feeds start from 2002. - for year in range(2002, current_year + 1): - self.stdout.write(f"Processing CPEs from year {year}") - download_url = nvd_base_url.format(year) - nvd_data = nvd_utils.fetch(download_url) - - vulnerabilities = list( - models.Vulnerability.objects.filter(vulnerability_id__startswith=f"CVE-{year}") - .prefetch_related("vulnerable_packages") - .prefetch_related("patched_packages") - ) - - vulnerabilities = { - vulnerability.vulnerability_id: vulnerability for vulnerability in vulnerabilities - } - purl_cpe_mapping = [] - - for cve_item in nvd_data["CVE_Items"]: - cve_id = cve_item["cve"]["CVE_data_meta"]["ID"] - if cve_id not in vulnerabilities: - continue - - purl_cpe_mapping.append({}) - purl_cpe_mapping[-1]["cve_id"] = cve_id - purl_cpe_mapping[-1]["purls"] = [] - purl_cpe_mapping[-1]["cpes"] = list(nvd_utils.extract_cpes(cve_item)) - - packages = self.get_packages( - vulnerabilities[cve_id], - options["vulnerable_purls_only"], - options["patched_purls_only"], - ) - for package in packages: - purl_cpe_mapping[-1]["purls"].append(package.package_url) - - if not os.path.exists("cpe2purl"): - os.mkdir("cpe2purl") - - with open(os.path.join("cpe2purl", f"{year}.json"), "w") as f: - json.dump(purl_cpe_mapping, f, indent=4) - - path = os.path.abspath("cpe2purl") - self.stdout.write(self.style.SUCCESS(f"Successfully created the mappings. Check {path}")) diff --git a/vulnerabilities/management/commands/purl2cpe.py b/vulnerabilities/management/commands/purl2cpe.py new file mode 100644 index 000000000..277348615 --- /dev/null +++ b/vulnerabilities/management/commands/purl2cpe.py @@ -0,0 +1,108 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/nexB/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +import json +import os +from collections import defaultdict + +import attr +from django.core.management.base import BaseCommand + +from vulnerabilities.models import Vulnerability + + +@attr.attributes +class Purl2Cpe: + vulnerablecode_id = attr.attrib(type=str) + cves = attr.attrib(default=attr.Factory(list), type=list) + purls = attr.attrib(default=attr.Factory(list), type=list, repr=False) + cpes = attr.attrib(default=attr.Factory(list), type=list, repr=False) + + def to_dict(self): + return attr.asdict(self) + + @classmethod + def collect(cls, limit=0, verbose=False): + """ + Yield Purl2Cpes collected from the current database. + Apply a limit of provided + """ + vulns = Vulnerability.objects.with_packages().with_cpes().distinct().all() + if limit: + vulns = vulns[:limit] + + for vuln in vulns: + if verbose: + print(f"Processing: {vuln.vulnerability_id}") + yield cls( + vulnerablecode_id=vuln.vulnerability_id, + cves=vuln.get_related_cves(), + purls=vuln.get_related_purls(), + cpes=vuln.get_related_cpes(), + ) + + @classmethod + def collect_by_years(cls, limit=0, verbose=False): + """ + Return a mapping of {CVE year: [list of Purl2Cpes]}. + Apply a limit of provided + """ + by_years = defaultdict(list) + for p2c in cls.collect(limit=limit, verbose=verbose): + for cve in p2c.cves: + try: + cve_year = cve.split("-")[1] + by_years[cve_year].append(p2c) + except Exception as e: + raise Exception(cve) from e + return by_years + + +class Command(BaseCommand): + """ + Dump JSON mappings of CPEs and Package URLs by vulnerability. + The process consists in these steps: + + - Iterate over all vulnerability with CPEs found in the VulnerableCode DB. + - Collect their CVEs, CPEs and purls joined together through the CVEs. + - Dump a list of Purl2Cpe grouped by year. + """ + + help = "Dump a mapping of CPEs to PURLs grouped by vulnerability." + + def add_arguments(self, parser): + + parser.add_argument( + "--limit", + default=0, + help="Limit the number of processed vulnerability", + ) + + parser.add_argument("destination", help="Destination directory") + + def handle(self, *args, **options): + limit = options["limit"] + if isinstance(limit, str): + limit = int(limit) + + destination = options["destination"] + assert destination, "Missing required estination directory" + destination = os.path.abspath(destination) + os.makedirs(destination, exist_ok=True) + + by_years = Purl2Cpe.collect_by_years(limit=limit) + + for year, purl2cpes in by_years.items(): + purl2cpes = [y.to_dict() for y in purl2cpes] + with open(os.path.join(destination, f"{year}.json"), "w") as out: + json.dump(purl2cpes, out, indent=2) + + print( + self.style.SUCCESS(f"Successfully dumped CPE to purl mappings in file://{destination}") + ) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index cbcea2051..716be5a44 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -54,6 +54,89 @@ def get_or_none(self, *args, **kwargs): return self.get(*args, **kwargs) +class VulnerabilityQuerySet(BaseQuerySet): + def with_cpes(self): + """ + Return a queryset of Vulnerability that have one or more NVD CPE references. + """ + return self.filter(vulnerabilityreference__reference_id__startswith="cpe") + + def for_cpe(self, cpe): + """ + Return a queryset of Vulnerability that have the ``cpe`` as an NVD CPE reference. + """ + return self.filter(vulnerabilityreference__reference_id__exact=cpe) + + def with_cves(self): + """ + Return a queryset of Vulnerability that have one or more NVD CVE aliases. + """ + return self.filter(aliases__alias__startswith="CVE") + + def for_cve(self, cve): + """ + Return a queryset of Vulnerability that have the the NVD CVE ``cve`` as an alias. + """ + return self.filter(vulnerabilityreference__reference_id__exact=cve) + + def with_packages(self): + """ + Return a queryset of Vulnerability that have one or more related packages. + """ + return self.filter(packages__isnull=False) + + def for_package(self, package): + """ + Return a queryset of Vulnerability related to ``package``. + """ + return self.filter(packages=package) + + def for_purl(self, package): + """ + Return a queryset of Vulnerability related to ``package``. + """ + return self.filter(packages=package) + + def search(self, query): + """ + Return a Vulnerability queryset searching for the ``query``. + Make a best effort approach to search a vulnerability. + """ + + qs = self + query = query and query.strip() + if not query: + return qs.none() + + # middle ground, exact on vulnerability_id + qssearch = qs.filter(vulnerability_id=query) + if not qssearch.exists(): + # middle ground, exact on alias + qssearch = qs.filter(aliases__alias=query) + if not qssearch.exists(): + # middle ground, slow enough + qssearch = qs.filter( + Q(vulnerability_id__icontains=query) | Q(aliases__alias__icontains=query) + ) + if not qssearch.exists(): + # last resort super slow + qssearch = qs.filter( + Q(references__id__icontains=query) | Q(summary__icontains=query) + ) + + return qssearch.order_by("vulnerability_id") + + def with_package_counts(self): + return self.annotate( + vulnerable_package_count=Count( + "packages", filter=Q(packagerelatedvulnerability__fix=False), distinct=True + ), + patched_package_count=Count( + "packages", filter=Q(packagerelatedvulnerability__fix=True), distinct=True + ), + ) + + class Vulnerability(models.Model): """ A software vulnerability with a unique identifier and alternate ``aliases``. @@ -82,6 +165,8 @@ class Vulnerability(models.Model): through="PackageRelatedVulnerability", ) + objects = VulnerabilityQuerySet.as_manager() + class Meta: verbose_name_plural = "Vulnerabilities" ordering = ["vulnerability_id"] @@ -89,39 +174,90 @@ class Meta: def __str__(self): return self.vulnerability_id + @property + def vcid(self): + return self.vulnerability_id + @property def severities(self): + """ + Return a queryset of VulnerabilitySeverity for this vulnerability. + """ return VulnerabilitySeverity.objects.filter(reference__in=self.references.all()) @property - def vulnerable_to(self): + def affected_packages(self): """ - Return packages that are vulnerable to this vulnerability. + Return a queryset of packages that are affected by this vulnerability. """ - return self.packages.vulnerable() + return self.packages.affected() + + # legacy aliases + vulnerable_packages = affected_packages @property - def resolved_to(self): + def fixed_by_packages(self): """ - Returns packages that first received patch against this vulnerability - in their particular version history. + Return a queryset of packages that are fixing this vulnerability. """ - return self.packages.filter(packagerelatedvulnerability__fix=True) + return self.packages.fixing() + + # legacy alias + patched_packages = fixed_by_packages @property - def alias(self): + def get_aliases(self): """ - Returns packages that first received patch against this vulnerability - in their particular version history. + Return a queryset of all Aliases for this vulnerability. """ return self.aliases.all() + alias = get_aliases + def get_absolute_url(self): """ - Return this Vulnerability details URL. + Return this Vulnerability details absolute URL. """ return reverse("vulnerability_details", args=[self.vulnerability_id]) + def get_related_cpes(self): + """ + Return a list of CPE strings of this vulnerability. + """ + return list(self.references.for_cpe().values_list("reference_id", flat=True).distinct()) + + def get_related_cves(self): + """ + Return a list of aliases CVE strings of this vulnerability. + """ + return list(self.aliases.for_cve().values_list("alias", flat=True).distinct()) + + def get_affected_purls(self): + """ + Return a list of purl strings affected by this vulnerability. + """ + return [p.package_url for p in self.affected_packages.all()] + + def get_fixing_purls(self): + """ + Return a list of purl strings fixing this vulnerability. + """ + return [p.package_url for p in self.fixed_by_packages.all()] + + def get_related_purls(self): + """ + Return a list of purl strings related to this vulnerability. + """ + return [p.package_url for p in self.packages.distinct().all()] + + +class VulnerabilityReferenceQuerySet(BaseQuerySet): + def for_cpe(self): + """ + Return a queryset of VulnerabilityReferences that are for a CPE. + """ + return self.filter(reference_id__startswith="cpe") + class VulnerabilityReference(models.Model): """ @@ -146,7 +282,7 @@ class VulnerabilityReference(models.Model): blank=True, ) - objects = BaseQuerySet.as_manager() + objects = VulnerabilityReferenceQuerySet.as_manager() class Meta: ordering = ["reference_id", "url"] @@ -155,6 +291,13 @@ def __str__(self): reference_id = f" {self.reference_id}" if self.reference_id else "" return f"{self.url}{reference_id}" + @property + def is_cpe(self): + """ + Return Trueis this is a CPE reference. + """ + return self.reference_id.startswith("cpe") + class VulnerabilityRelatedReference(models.Model): """ @@ -202,12 +345,20 @@ def for_package_url_object(self, purl): else: return self.none() - def vulnerable(self): + def affected(self): """ - Return all vulnerable packages. + Return only packages affected by a vulnerability. """ return self.filter(packagerelatedvulnerability__fix=False) + vulnerable = affected + + def fixing(self): + """ + Return only packages fixing a vulnerability . + """ + return self.filter(packagerelatedvulnerability__fix=True) + def with_vulnerability_counts(self): return self.annotate( vulnerability_count=Count( @@ -220,6 +371,111 @@ def with_vulnerability_counts(self): ), ) + def fixing_packages(self, package, with_qualifiers_and_subpath=True): + """ + Return a queryset of packages that are fixing the vulnerability of + ``package``. + """ + + return self.match_purl( + purl=package.purl, + with_qualifiers_and_subpath=with_qualifiers_and_subpath, + ).fixing() + + def search(self, query=None): + """ + Return a Package queryset searching for the ``query``. + Make a best effort approach to find matching packages either based + on exact purl, partial purl or just name and namespace. + """ + query = query and query.strip() + if not query: + return self.none() + + qs = self + if not query.startswith("pkg:"): + # treat this as a plain search + qs = qs.filter(Q(name__icontains=query) | Q(namespace__icontains=query)) + else: + # this looks like a purl: check if it quacks like a purl + purl_type = namespace = name = version = None + + _, _scheme, remainder = query.partition("pkg:") + remainder = remainder.strip() + if not remainder: + return qs.none() + + try: + # First, treat the query as a syntactically-correct purl + purl = PackageURL.from_string(query) + purl_type, namespace, name, version, _quals, _subp = purl.to_dict().values() + except ValueError: + # Otherwise, attempt a more lenient parsing of a possibly partial purl + if "/" in remainder: + purl_type, _scheme, ns_name = remainder.partition("/") + ns_name = ns_name.strip() + if ns_name: + if "/" in ns_name: + namespace, _, name = ns_name.partition("/") + else: + name = ns_name + name = name.strip() + if name: + if "@" in name: + name, _, version = name.partition("@") + version = version.strip() + name = name.strip() + else: + purl_type = remainder + + if purl_type: + qs = qs.filter(type__iexact=purl_type) + if namespace: + qs = qs.filter(namespace__iexact=namespace) + if name: + qs = qs.filter(name__iexact=name) + if version: + qs = qs.filter(version__iexact=version) + + return qs + + def for_purl(self, purl, with_qualifiers_and_subpath=True): + """ + Return a queryset matching the ``purl`` Package URL. + """ + if not isinstance(purl, PackageURL): + purl = PackageURL.from_string(purl) + purl = purl.to_dict() + if not with_qualifiers_and_subpath: + del purl["qualifiers"] + del purl["subpath"] + return self.filter(**purl) + + def with_cpes(self): + """ + Return a queryset of Package that a vulnerability with one or more NVD CPE references. + """ + return self.filter(vulnerabilities__vulnerabilityreference__reference_id__startswith="cpe") + + def for_cpe(self, cpe): + """ + Return a queryset of Vulnerability that have the ``cpe`` as an NVD CPE reference. + """ + return self.filter(vulnerabilities__vulnerabilityreference__reference_id__exact=cpe) + + def with_cves(self): + """ + Return a queryset of Vulnerability that have one or more NVD CVE aliases. + """ + return self.filter(vulnerabilities__aliases__alias__startswith="CVE") + + def for_cve(self, cve): + """ + Return a queryset of Vulnerability that have the the NVD CVE ``cve`` as an alias. + """ + return self.filter(vulnerabilities__vulnerabilityreference__reference_id__exact=cve) + + def get_purl_query_lookups(purl): """ @@ -270,40 +526,39 @@ def __str__(self): @property # TODO: consider renaming to "affected_by" - def vulnerable_to(self): + def affected_by(self): """ - Returns vulnerabilities which are affecting this package. + Return a queryset of vulnerabilities affecting this package. """ return self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) + # legacy aliases + vulnerable_to = affected_by + @property # TODO: consider renaming to "fixes" or "fixing" ? (TBD) and updating the docstring - def resolved_to(self): + def fixing(self): """ - Returns the vulnerabilities which this package is patched against. + Return a queryset of vulnerabilities fixed by this package. """ return self.vulnerabilities.filter(packagerelatedvulnerability__fix=True) + # legacy aliases + resolved_to = fixing + @property def fixed_packages(self): """ - Returns vulnerabilities which are affecting this package. + Return a queryset of packages that are fixed. """ - return Package.objects.filter( - name=self.name, - namespace=self.namespace, - type=self.type, - qualifiers=self.qualifiers, - subpath=self.subpath, - packagerelatedvulnerability__fix=True, - ).distinct() + return Package.objects.fixing_packages(package=self).distinct() @property def is_vulnerable(self) -> bool: """ Returns True if this package is vulnerable to any vulnerability. """ - return self.vulnerable_to.exists() + return self.affected_by.exists() def get_absolute_url(self): """ @@ -313,6 +568,9 @@ def get_absolute_url(self): class PackageRelatedVulnerability(models.Model): + """ + Track the relationship between a Package and Vulnerability. + """ # TODO: Fix related_name package = models.ForeignKey( @@ -324,6 +582,7 @@ class PackageRelatedVulnerability(models.Model): Vulnerability, on_delete=models.CASCADE, ) + created_by = models.CharField( max_length=100, blank=True, @@ -410,6 +669,14 @@ class Meta: ordering = ["reference", "scoring_system", "value"] +class AliasQuerySet(BaseQuerySet): + def for_cve(self): + """ + Return a queryset of Aliases that are for a CVE. + """ + return self.filter(alias__startswith="CVE") + + class Alias(models.Model): """ An alias is a unique vulnerability identifier in some database, such as @@ -434,6 +701,8 @@ class Alias(models.Model): related_name="aliases", ) + objects = AliasQuerySet.as_manager() + class Meta: ordering = ["alias"] diff --git a/vulnerabilities/templates/vulnerability_details.html b/vulnerabilities/templates/vulnerability_details.html index 4d1108a69..902270e0c 100644 --- a/vulnerabilities/templates/vulnerability_details.html +++ b/vulnerabilities/templates/vulnerability_details.html @@ -34,14 +34,14 @@
  • - Fixed by packages ({{ resolved_to|length }}) + Fixed by packages ({{ fixed_by_packages|length }})
  • - Affected packages ({{ vulnerable_to|length }}) + Affected packages ({{ affected_packages|length }})
  • @@ -118,11 +118,11 @@
    - Fixed by packages ({{ resolved_to|length }}) + Fixed by packages ({{ fixed_by_packages|length }})
    - {% for package in resolved_to|slice:":3" %} + {% for package in fixed_by_packages|slice:":3" %} {% endfor %} - {% if resolved_to|length > 3 %} + {% if fixed_by_packages|length > 3 %}
    {{ package.purl }} @@ -136,7 +136,7 @@
    ... see Fixed by packages tab for more @@ -147,11 +147,11 @@
    - Affected packages ({{ vulnerable_to|length }}) + Affected packages ({{ affected_packages|length }})
    - {% for package in vulnerable_to|slice:":3" %} + {% for package in affected_packages|slice:":3" %} {% endfor %} - {% if vulnerable_to|length > 3 %} + {% if affected_packages|length > 3 %} - {% for package in vulnerable_to %} + {% for package in affected_packages %} - {% for package in resolved_to %} + {% for package in fixed_by_packages %}
    {{ package.purl }} @@ -165,7 +165,7 @@
    ... see Affected packages tab for more @@ -215,7 +215,7 @@
    {{ package.purl }} @@ -244,7 +244,7 @@
    {{ package.purl }} diff --git a/vulnerabilities/tests/test_nvd.py b/vulnerabilities/tests/test_nvd.py index fad4fdce8..6b46a3c03 100644 --- a/vulnerabilities/tests/test_nvd.py +++ b/vulnerabilities/tests/test_nvd.py @@ -10,11 +10,7 @@ import json import os -from vulnerabilities.importers.nvd import extract_cpes -from vulnerabilities.importers.nvd import extract_reference_urls -from vulnerabilities.importers.nvd import extract_summary -from vulnerabilities.importers.nvd import related_to_hardware -from vulnerabilities.importers.nvd import to_advisories +from vulnerabilities.importers import nvd BASE_DIR = os.path.dirname(os.path.abspath(__file__)) TEST_DATA = os.path.join(BASE_DIR, "test_data/nvd/nvd_test.json") @@ -25,10 +21,24 @@ def load_test_data(): return json.load(f) -def test_nvd_importer_with_hardware(regen=False): +def sorted_advisory_data(advisory_data): + """ + Sorted nested lists in a list of AdvisoryData mappings. + """ + sorter = lambda dct: tuple(dct.items()) + for data in advisory_data: + data["aliases"] = sorted(data["aliases"]) + data["affected_packages"] = sorted(data["affected_packages"], key=sorter) + data["references"] = sorted(data["references"], key=sorter) + return advisory_data + + +def test_to_advisories_skips_hardware(regen=False): expected_file = os.path.join(BASE_DIR, "test_data/nvd/nvd-expected.json") - result = [data.to_dict() for data in list(to_advisories(load_test_data()))] + test_data = load_test_data() + result = [data.to_dict() for data in nvd.to_advisories(test_data)] + result = sorted_advisory_data(result) if regen: with open(expected_file, "w") as f: @@ -37,11 +47,13 @@ def test_nvd_importer_with_hardware(regen=False): else: with open(expected_file) as f: expected = json.load(f) + expected = sorted_advisory_data(expected) assert result == expected -def get_cve_item(): +# TODO: use a JSON fixtures instead +def get_test_cve_item(): return { "cve": { @@ -127,49 +139,38 @@ def get_cve_item(): } -def test_extract_cpes(): - expected_cpes = { +def test_CveItem_cpes(): + expected_cpes = [ "cpe:2.3:a:csilvers:gperftools:0.1:*:*:*:*:*:*:*", "cpe:2.3:a:csilvers:gperftools:0.2:*:*:*:*:*:*:*", "cpe:2.3:a:csilvers:gperftools:*:*:*:*:*:*:*:*", - } - - found_cpes = set() - found_cpes.update(extract_cpes(get_cve_item())) + ] + found_cpes = nvd.CveItem(cve_item=get_test_cve_item()).cpes assert found_cpes == expected_cpes -def test_related_to_hardware(): - assert ( - related_to_hardware( - cpes=[ - "cpe:2.3:a:csilvers:gperftools:0.1:*:*:*:*:*:*:*", - "cpe:2.3:h:csilvers:gperftools:0.2:*:*:*:*:*:*:*", - "cpe:2.3:a:csilvers:gperftools:*:*:*:*:*:*:*:*", - ] - ) - == True - ) +def test_is_related_to_hardware(): + assert nvd.is_related_to_hardware("cpe:2.3:h:csilvers:gperftools:0.2:*:*:*:*:*:*:*") + assert not nvd.is_related_to_hardware("cpe:2.3:a:csilvers:gperftools:0.1:*:*:*:*:*:*:*") + assert not nvd.is_related_to_hardware("cpe:2.3:a:csilvers:gperftools:*:*:*:*:*:*:*:*") -def test_extract_summary_with_single_summary(): +def test_CveItem_summary_with_single_summary(): expected_summary = ( "Multiple integer overflows in TCMalloc (tcmalloc.cc) in gperftools " "before 0.4 make it easier for context-dependent attackers to perform memory-related " "attacks such as buffer overflows via a large size value, which causes less memory to " "be allocated than expected." ) - found_summary = extract_summary(get_cve_item()) - assert found_summary == expected_summary + + assert nvd.CveItem(cve_item=get_test_cve_item()).summary == expected_summary -def test_extract_reference_urls(): - expected_urls = { +def test_CveItem_reference_urls(): + expected_urls = [ "http://code.google.com/p/gperftools/source/browse/tags/perftools-0.4/ChangeLog", "http://kqueue.org/blog/2012/03/05/memory-allocator-security-revisited/", - } - - found_urls = extract_reference_urls(get_cve_item()) + ] - assert found_urls == expected_urls + assert nvd.CveItem(cve_item=get_test_cve_item()).reference_urls == expected_urls diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index d9ff4331f..06dcd1327 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -8,8 +8,6 @@ # from django.core.mail import send_mail -from django.db.models import Count -from django.db.models import Q from django.http import HttpResponse from django.http.response import Http404 from django.shortcuts import render @@ -18,7 +16,6 @@ from django.views import generic from django.views.generic.detail import DetailView from django.views.generic.list import ListView -from packageurl import PackageURL from vulnerabilities import models from vulnerabilities.forms import ApiUserCreationForm @@ -43,72 +40,8 @@ def get_context_data(self, **kwargs): return context def get_queryset(self, query=None): - """ - Return a Package queryset for the ``query``. - Make a best effort approach to find matching packages either based - on exact purl, partial purl or just name and namespace. - """ - qs = self.model.objects - query = query or self.request.GET.get("search") or "" - query = query.strip() - if not query: - return qs.none() - - if not query.startswith("pkg:"): - # treat this as a plain search - qs = qs.filter(Q(name__icontains=query) | Q(namespace__icontains=query)) - else: - # this looks like a purl: check if it quacks like a purl - purl_type = namespace = name = version = None - - _, _scheme, remainder = query.partition("pkg:") - remainder = remainder.strip() - if not remainder: - return qs.none() - - try: - # First, treat the query as a syntactically-correct purl - purl = PackageURL.from_string(query) - purl_type, namespace, name, version, _quals, _subp = purl.to_dict().values() - except ValueError: - # Otherwise, attempt a more lenient parsing of a possibly partial purl - if "/" in remainder: - purl_type, _scheme, ns_name = remainder.partition("/") - ns_name = ns_name.strip() - if ns_name: - if "/" in ns_name: - namespace, _, name = ns_name.partition("/") - else: - name = ns_name - name = name.strip() - if name: - if "@" in name: - name, _, version = name.partition("@") - version = version.strip() - name = name.strip() - else: - purl_type = remainder - - if purl_type: - qs = qs.filter(type__iexact=purl_type) - if namespace: - qs = qs.filter(namespace__iexact=namespace) - if name: - qs = qs.filter(name__iexact=name) - if version: - qs = qs.filter(version__iexact=version) - - return qs.annotate( - vulnerability_count=Count( - "vulnerabilities", - filter=Q(packagerelatedvulnerability__fix=False), - ), - patched_vulnerability_count=Count( - "vulnerabilities", - filter=Q(packagerelatedvulnerability__fix=True), - ), - ).prefetch_related() + return self.model.objects.search(query).with_vulnerability_counts().prefetch_related() class VulnerabilitySearch(ListView): @@ -126,35 +59,7 @@ def get_context_data(self, **kwargs): def get_queryset(self, query=None): query = query or self.request.GET.get("search") or "" - qs = self.model.objects - query = query.strip() - if not query: - return qs.none() - - # middle ground, exact on vulnerability_id - qssearch = qs.filter(vulnerability_id=query) - if not qssearch.exists(): - # middle ground, exact on alias - qssearch = qs.filter(aliases__alias=query) - if not qssearch.exists(): - # middle ground, slow enough - qssearch = qs.filter( - Q(vulnerability_id__icontains=query) | Q(aliases__alias__icontains=query) - ) - if not qssearch.exists(): - # last resort super slow - qssearch = qs.filter( - Q(references__id__icontains=query) | Q(summary__icontains=query) - ) - - return qssearch.order_by("vulnerability_id").annotate( - vulnerable_package_count=Count( - "packages", filter=Q(packagerelatedvulnerability__fix=False), distinct=True - ), - patched_package_count=Count( - "packages", filter=Q(packagerelatedvulnerability__fix=True), distinct=True - ), - ) + return self.model.objects.search(query=query).with_package_counts() class PackageDetails(DetailView): @@ -167,8 +72,8 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) package = self.object context["package"] = package - context["affected_by_vulnerabilities"] = package.vulnerable_to.order_by("vulnerability_id") - context["fixing_vulnerabilities"] = package.resolved_to.order_by("vulnerability_id") + context["affected_by_vulnerabilities"] = package.affected_by.order_by("vulnerability_id") + context["fixing_vulnerabilities"] = package.fixing.order_by("vulnerability_id") context["package_search_form"] = PackageSearchForm(self.request.GET) return context @@ -210,8 +115,8 @@ def get_context_data(self, **kwargs): "severities": list(self.object.severities), "references": self.object.references.all(), "aliases": self.object.aliases.all(), - "resolved_to": self.object.resolved_to.all(), - "vulnerable_to": self.object.vulnerable_to.all(), + "affected_packages": self.object.affected_packages.all(), + "fixed_by_packages": self.object.fixed_by_packages.all(), } ) return context From 805c1fad5489d3b778f76863ab57f80122b39964 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Sun, 30 Oct 2022 22:07:49 +0100 Subject: [PATCH 24/27] Format models Signed-off-by: Philippe Ombredanne --- vulnerabilities/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 716be5a44..49c80866d 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -476,7 +476,6 @@ def for_cve(self, cve): return self.filter(vulnerabilities__vulnerabilityreference__reference_id__exact=cve) - def get_purl_query_lookups(purl): """ Do not reference all the possible qualifiers and relax the From fe1e9bb123db861cbfe219856ffbe41a2be7b65d Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 8 Nov 2022 22:25:23 +0100 Subject: [PATCH 25/27] Format settings Signed-off-by: Philippe Ombredanne --- vulnerablecode/settings.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index f85c0098c..23bf0ca0d 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -65,11 +65,11 @@ "vulnerabilities", # Django built-in "django.contrib.auth", + "django.contrib.admin", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "django.contrib.admin", "django.contrib.humanize", # Third-party apps "django_filters", @@ -161,6 +161,10 @@ USE_I18N = True +USE_L10N = True + +USE_TZ = True + IS_TESTS = False if len(sys.argv) > 0: @@ -195,9 +199,6 @@ "bulk_search_cpes": "5/day", } -USE_L10N = True - -USE_TZ = True # Static files (CSS, JavaScript, Images) From e4d508ee2f0121bc97828faa3e9e5c3f6c82c18e Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 8 Nov 2022 23:26:05 +0100 Subject: [PATCH 26/27] Merge latest main branch Signed-off-by: Philippe Ombredanne --- vulnerabilities/api.py | 17 ++++++++++--- vulnerabilities/forms.py | 8 +++--- vulnerabilities/models.py | 2 -- .../templates/api_user_creation_form.html | 7 ++++++ vulnerabilities/tests/test_auth.py | 4 +-- vulnerabilities/views.py | 25 +++++++++++++++---- vulnerablecode/settings.py | 13 ++++++---- 7 files changed, 53 insertions(+), 23 deletions(-) diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index e9450bca9..ddac03016 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -23,6 +23,7 @@ from vulnerabilities.models import VulnerabilityReference from vulnerabilities.models import VulnerabilitySeverity from vulnerabilities.models import get_purl_query_lookups +from vulnerabilities.throttling import StaffUserRateThrottle class VulnerabilitySeveritySerializer(serializers.ModelSerializer): @@ -228,9 +229,11 @@ class PackageViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = PackageSerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = PackageFilterSet + throttle_classes = [StaffUserRateThrottle] + throttle_scope = "packages" # TODO: Fix the swagger documentation for this endpoint - @action(detail=False, methods=["post"]) + @action(detail=False, methods=["post"], throttle_scope="bulk_search_packages") def bulk_search(self, request): """ Lookup for vulnerable packages using many Package URLs at once. @@ -254,7 +257,7 @@ def bulk_search(self, request): if purl_data: purl_response = PackageSerializer(purl_data[0], context={"request": request}).data else: - purl_response = purl + purl_response = purl.to_dict() purl_response["unresolved_vulnerabilities"] = [] purl_response["resolved_vulnerabilities"] = [] purl_response["purl"] = purl_string @@ -262,7 +265,7 @@ def bulk_search(self, request): return Response(response) - @action(detail=False, methods=["get"]) + @action(detail=False, methods=["get"], throttle_scope="vulnerable_packages") def all(self, request): """ Return the Package URLs of all packages known to be vulnerable. @@ -314,6 +317,8 @@ def get_queryset(self): serializer_class = VulnerabilitySerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = VulnerabilityFilterSet + throttle_classes = [StaffUserRateThrottle] + throttle_scope = "vulnerabilities" class CPEFilterSet(filters.FilterSet): @@ -334,9 +339,11 @@ class CPEViewSet(viewsets.ReadOnlyModelViewSet): ).distinct() serializer_class = VulnerabilitySerializer filter_backends = (filters.DjangoFilterBackend,) + throttle_classes = [StaffUserRateThrottle] filterset_class = CPEFilterSet + throttle_scope = "cpes" - @action(detail=False, methods=["post"]) + @action(detail=False, methods=["post"], throttle_scope="bulk_search_cpes") def bulk_search(self, request): """ Lookup for vulnerabilities using many CPEs at once. @@ -378,3 +385,5 @@ class AliasViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = VulnerabilitySerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = AliasFilterSet + throttle_classes = [StaffUserRateThrottle] + throttle_scope = "aliases" diff --git a/vulnerabilities/forms.py b/vulnerabilities/forms.py index aff010d19..566d94543 100644 --- a/vulnerabilities/forms.py +++ b/vulnerabilities/forms.py @@ -41,24 +41,24 @@ class ApiUserCreationForm(forms.ModelForm): class Meta: model = ApiUser fields = ( - "username", + "email", "first_name", "last_name", ) def __init__(self, *args, **kwargs): super(ApiUserCreationForm, self).__init__(*args, **kwargs) - self.fields["username"].help_text = f"
    • {self.fields['username'].help_text}
    " + self.fields["email"].required = True def save(self, commit=True): return ApiUser.objects.create_api_user( - username=self.cleaned_data["username"], + username=self.cleaned_data["email"], first_name=self.cleaned_data["first_name"], last_name=self.cleaned_data["last_name"], ) def clean_username(self): - username = self.cleaned_data["username"] + username = self.cleaned_data["email"] validate_email(username) return username diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 49c80866d..d07b6c2a8 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -12,7 +12,6 @@ import logging from contextlib import suppress -from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import UserManager from django.core import exceptions @@ -24,7 +23,6 @@ from django.db.models import Q from django.db.models.functions import Length from django.db.models.functions import Trim -from django.dispatch import receiver from django.urls import reverse from packageurl import PackageURL from packageurl.contrib.django.models import PackageURLMixin diff --git a/vulnerabilities/templates/api_user_creation_form.html b/vulnerabilities/templates/api_user_creation_form.html index 69f0f5489..acd473016 100644 --- a/vulnerabilities/templates/api_user_creation_form.html +++ b/vulnerabilities/templates/api_user_creation_form.html @@ -4,6 +4,13 @@ {% block content %}
    + {% for message in messages %} +
    +
    + {{ message|linebreaksbr }} +
    +
    + {% endfor %} {% block title %} VulnerableCode API key request {% endblock %} diff --git a/vulnerabilities/tests/test_auth.py b/vulnerabilities/tests/test_auth.py index 8a597cbbd..131bafd39 100644 --- a/vulnerabilities/tests/test_auth.py +++ b/vulnerabilities/tests/test_auth.py @@ -24,9 +24,7 @@ class VulnerableCodeAuthTest(TestCase): def setUp(self): - self.basic_user = ApiUser.objects.create_api_user( - username="basic_user@foo.com", password=TEST_PASSWORD - ) + self.basic_user = ApiUser.objects.create_api_user(username="basic_user@foo.com") def test_vulnerablecode_auth_api_required_authentication(self): response = self.client.get(api_package_url) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 06dcd1327..6b761a9fc 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -7,9 +7,11 @@ # See https://aboutcode.org for more information about nexB OSS projects. # +from django.contrib import messages +from django.core.exceptions import ValidationError from django.core.mail import send_mail -from django.http import HttpResponse from django.http.response import Http404 +from django.shortcuts import redirect from django.shortcuts import render from django.urls import reverse_lazy from django.views import View @@ -40,6 +42,11 @@ def get_context_data(self, **kwargs): return context def get_queryset(self, query=None): + """ + Return a Package queryset for the ``query``. + Make a best effort approach to find matching packages either based + on exact purl, partial purl or just name and namespace. + """ query = query or self.request.GET.get("search") or "" return self.model.objects.search(query).with_vulnerability_counts().prefetch_related() @@ -140,18 +147,26 @@ class ApiUserCreateView(generic.CreateView): template_name = "api_user_creation_form.html" def form_valid(self, form): - super().form_valid(form) + + try: + response = super().form_valid(form) + except ValidationError: + messages.error(self.request, "Email is invalid or already taken") + return redirect(self.get_success_url()) send_mail( subject="VulnerableCode.io API key token", message=f"Here is your VulnerableCode.io API key token: {self.object.auth_token}", from_email=env.str("FROM_EMAIL", default=""), recipient_list=[self.object.email], + fail_silently=True, ) - return HttpResponse( - f"Check your email for VulnerableCode.io API key token: {self.object.email}" + messages.success( + self.request, f"API key token sent to your email address {self.object.email}." ) + return response + def get_success_url(self): - return reverse_lazy("home") + return reverse_lazy("api_user_request") diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index 23bf0ca0d..6ad02f61f 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -65,11 +65,11 @@ "vulnerabilities", # Django built-in "django.contrib.auth", - "django.contrib.admin", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.contrib.admin", "django.contrib.humanize", # Third-party apps "django_filters", @@ -161,10 +161,6 @@ USE_I18N = True -USE_L10N = True - -USE_TZ = True - IS_TESTS = False if len(sys.argv) > 0: @@ -200,6 +196,10 @@ } +USE_L10N = True + +USE_TZ = True + # Static files (CSS, JavaScript, Images) STATIC_URL = "/static/" @@ -210,6 +210,7 @@ str(PROJECT_DIR / "static"), ] + CRISPY_TEMPLATE_PACK = "bootstrap4" # Third-party apps @@ -293,9 +294,11 @@ "TAGS_SORTER": False, } + if not VULNERABLECODEIO_REQUIRE_AUTHENTICATION: REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.AllowAny",) + if DEBUG_TOOLBAR: INSTALLED_APPS += ("debug_toolbar",) From 464adb36503ef31750367871f669422c095bdd37 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 8 Nov 2022 23:49:31 +0100 Subject: [PATCH 27/27] Bump version Signed-off-by: Philippe Ombredanne --- CHANGELOG.rst | 12 ++++++++++-- setup.cfg | 2 +- vulnerablecode/__init__.py | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 414cae374..56beed61a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,14 +3,22 @@ Release notes -Version v30.2.2 +Version v30.3.0 ---------------- - We enabled API throttling for a basic user and for a staff user they can have unlimited access on API. - We added throttle rate for each API endpoint and it can be - configured from the settings #991 https://github.com/nexB/vulnerablecode/issues/991. + configured from the settings #991 https://github.com/nexB/vulnerablecode/issues/991 + +- We improved how we import NVD data +- We refactored and made the purl2cpe script work to dump purl to CPE mappings + +Internally: + +- We aligned key names internally with the names used in the UI and API (such as affected and fixed) +- We now use querysets as model managers and have streamlined view code Version v30.2.1 diff --git a/setup.cfg b/setup.cfg index 72ed76110..dd849fad6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = vulnerablecode -version = 30.2.1 +version = 30.3.0 license = Apache-2.0 AND CC-BY-SA-4.0 # description must be on ONE line https://github.com/pypa/setuptools/issues/1390 diff --git a/vulnerablecode/__init__.py b/vulnerablecode/__init__.py index 1e68626f1..e415fce2e 100644 --- a/vulnerablecode/__init__.py +++ b/vulnerablecode/__init__.py @@ -12,7 +12,7 @@ import warnings from pathlib import Path -__version__ = "30.2.1" +__version__ = "30.3.0" def command_line():