Skip to content

Commit c9f70a0

Browse files
authored
Merge pull request #987 from nexB/api-authentication
Add API authentication, key request and documentation
2 parents eb6e935 + 86d5f86 commit c9f70a0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+971
-1317
lines changed

docs/source/api.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.. _api:
2+
3+
API overview
4+
========================
5+
6+
7+
Browse the Open API documentation
8+
------------------------------------
9+
10+
- https://public.vulnerablecode.io/api/docs/ for documentation with Swagger
11+
- https://public.vulnerablecode.io/api/schema/ for the OpenAPI schema
12+
13+
14+
Enable the API key authentication
15+
------------------------------------
16+
17+
There is a setting VULNERABLECODEIO_REQUIRE_AUTHENTICATION for this. Use it this
18+
way::
19+
20+
$ VULNERABLECODEIO_REQUIRE_AUTHENTICATION=1 make run
21+
22+
23+
Create an API key-only user
24+
------------------------------------
25+
26+
This can be done in the admin and from the command line::
27+
28+
$ ./manage.py create_api_user --email "[email protected]" --first-name="Phil" --last-name "Goel"
29+
User [email protected] created with API key: ce8616b929d2adsddd6146346c2f26536423423491
30+
31+
32+
Access the API using curl
33+
-----------------------------
34+
35+
curl -X GET -H 'Authorization: Token <YOUR TOKEN>' https://public.vulnerablecode.io/api/
36+
37+
38+
API endpoints
39+
---------------
40+
41+
42+
There are two primary endpoints:
43+
44+
- packages/: this is the main endpoint where you can lookup vulnerabilities by package.
45+
46+
- vulnerabilities/: to lookup by vulnerabilities
47+
48+
And two secondary endpoints, used to query vulnerability aliases (such as CVEs)
49+
and vulnerability by CPEs: cpes/ and aliases/
50+

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ In this documentation you will find information on:
4343
reference_framework_overview
4444
command-line-interface
4545
importers_link
46+
api
4647

4748
.. toctree::
4849
:maxdepth: 1

requirements.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ decorator==5.1.1
2121
defusedxml==0.7.1
2222
distro==1.7.0
2323
Django==4.0.7
24+
django-crispy-forms==1.10.0
2425
django-environ==0.8.1
2526
django-filter==21.1
2627
django-widget-tweaks==1.4.12
@@ -65,7 +66,7 @@ platformdirs==2.5.1
6566
pluggy==1.0.0
6667
pprintpp==0.4.0
6768
prompt-toolkit==3.0.29
68-
psycopg2==2.9.3
69+
psycopg2-binary==2.9.3
6970
ptyprocess==0.7.0
7071
pure-eval==0.2.2
7172
py==1.11.0
@@ -114,3 +115,9 @@ yarl==1.7.2
114115
zipp==3.8.0
115116
dateparser==1.1.1
116117
fetchcode==0.2.0
118+
119+
drf-spectacular-sidecar==2022.10.1
120+
drf-spectacular==0.24.2
121+
coreapi==2.3.3
122+
coreschema==0.0.4
123+
itypes==1.2.0

setup.cfg

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,18 @@ zip_safe = false
5656

5757
install_requires =
5858
Django>=4.0.0
59-
psycopg2>=2.8.6
59+
psycopg2-binary>=2.8.6
6060
djangorestframework>=3.12.4
6161
django-filter>=2.4.0
6262
django-widget-tweaks>=1.4.8
63+
django-crispy-forms>=1.10.0
6364
django-environ>=0.8.0
6465
gunicorn>=20.1.0
6566

67+
# for the API doc
68+
drf-spectacular[sidecar]>=0.24.2
69+
coreapi>=2.3.3
70+
6671
#essentials
6772
packageurl-python>=0.9.4
6873
univers>=30.9.0

vulnerabilities/admin.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
from django import forms
1011
from django.contrib import admin
12+
from django.core.validators import validate_email
1113

14+
from vulnerabilities.models import ApiUser
1215
from vulnerabilities.models import Package
1316
from vulnerabilities.models import PackageRelatedVulnerability
1417
from vulnerabilities.models import Vulnerability
@@ -41,3 +44,63 @@ class PackageRelatedVulnerabilityAdmin(admin.ModelAdmin):
4144
@admin.register(VulnerabilitySeverity)
4245
class VulnerabilitySeverityAdmin(admin.ModelAdmin):
4346
pass
47+
48+
49+
class ApiUserCreationForm(forms.ModelForm):
50+
"""
51+
This helps have a simplified creation for API-only users in the admin
52+
"""
53+
54+
class Meta:
55+
model = ApiUser
56+
fields = (
57+
"username",
58+
"first_name",
59+
"last_name",
60+
)
61+
62+
def save(self, commit=True):
63+
return ApiUser.objects.create_api_user(
64+
username=self.cleaned_data["username"],
65+
first_name=self.cleaned_data["first_name"],
66+
last_name=self.cleaned_data["last_name"],
67+
)
68+
69+
def clean_username(self):
70+
username = self.cleaned_data["username"]
71+
validate_email(username)
72+
return username
73+
74+
def save_m2m(self):
75+
pass
76+
77+
78+
@admin.register(ApiUser)
79+
class ApiUserAdmin(admin.ModelAdmin):
80+
list_display = ("username", "email", "first_name", "last_name", "is_staff")
81+
list_filter = ("username", "email", "first_name", "last_name", "is_staff")
82+
search_fields = ("username", "email", "first_name", "last_name")
83+
fieldsets = (
84+
(
85+
None,
86+
{
87+
"fields": (
88+
"username",
89+
"first_name",
90+
"last_name",
91+
)
92+
},
93+
),
94+
)
95+
96+
add_form = ApiUserCreationForm
97+
98+
def get_form(self, request, obj=None, **kwargs):
99+
"""
100+
Use special form during user creation
101+
"""
102+
defaults = {}
103+
if obj is None:
104+
defaults["form"] = self.add_form
105+
defaults.update(kwargs)
106+
return super().get_form(request, obj, **defaults)

vulnerabilities/api.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class Meta:
5555

5656
class VulnSerializerRefsAndSummary(serializers.HyperlinkedModelSerializer):
5757
"""
58-
Used for nesting inside package focused APIs.
58+
Lookup vulnerabilities references by aliases (such as a CVE).
5959
"""
6060

6161
fixed_packages = MinimalPackageSerializer(
@@ -71,7 +71,7 @@ class Meta:
7171

7272
class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
7373
"""
74-
Used for nesting inside package focused APIs.
74+
Lookup vulnerabilities by aliases (such as a CVE).
7575
"""
7676

7777
class Meta:
@@ -113,6 +113,10 @@ class Meta:
113113

114114

115115
class PackageSerializer(serializers.HyperlinkedModelSerializer):
116+
"""
117+
Lookup software package using Package URLs
118+
"""
119+
116120
def to_representation(self, instance):
117121
data = super().to_representation(instance)
118122
data["unresolved_vulnerabilities"] = data["affected_by_vulnerabilities"]
@@ -138,9 +142,9 @@ def get_fixed_packages(self, package):
138142
packagerelatedvulnerability__fix=True,
139143
).distinct()
140144

141-
def get_vulnerabilities_for_a_package(self, package, fix):
145+
def get_vulnerabilities_for_a_package(self, package, fix) -> dict:
142146
"""
143-
Return a queryset of vulnerabilities related to the given `package`.
147+
Return a mapping of vulnerabilities data related to the given `package`.
144148
Return vulnerabilities that affects the `package` if given `fix` flag is False,
145149
otherwise return vulnerabilities fixed by the `package`.
146150
"""
@@ -159,15 +163,15 @@ def get_vulnerabilities_for_a_package(self, package, fix):
159163
context={"request": self.context["request"]},
160164
).data
161165

162-
def get_fixed_vulnerabilities(self, package):
166+
def get_fixed_vulnerabilities(self, package) -> dict:
163167
"""
164-
Return a queryset of vulnerabilities fixed in the given `package`.
168+
Return a mapping of vulnerabilities fixed in the given `package`.
165169
"""
166170
return self.get_vulnerabilities_for_a_package(package=package, fix=True)
167171

168-
def get_affected_vulnerabilities(self, package):
172+
def get_affected_vulnerabilities(self, package) -> dict:
169173
"""
170-
Return a queryset of vulnerabilities that affects the given `package`.
174+
Return a mapping of vulnerabilities that affects the given `package`.
171175
"""
172176
return self.get_vulnerabilities_for_a_package(package=package, fix=False)
173177

@@ -217,6 +221,10 @@ def filter_purl(self, queryset, name, value):
217221

218222

219223
class PackageViewSet(viewsets.ReadOnlyModelViewSet):
224+
"""
225+
Lookup for vulnerable packages by Package URL.
226+
"""
227+
220228
queryset = Package.objects.all()
221229
serializer_class = PackageSerializer
222230
filter_backends = (filters.DjangoFilterBackend,)
@@ -228,7 +236,7 @@ class PackageViewSet(viewsets.ReadOnlyModelViewSet):
228236
@action(detail=False, methods=["post"], throttle_scope="bulk_search_packages")
229237
def bulk_search(self, request):
230238
"""
231-
See https://github.com/nexB/vulnerablecode/pull/369#issuecomment-796877606 for docs
239+
Lookup for vulnerable packages using many Package URLs at once.
232240
"""
233241
response = []
234242
purls = request.data.get("purls", []) or []
@@ -260,7 +268,7 @@ def bulk_search(self, request):
260268
@action(detail=False, methods=["get"], throttle_scope="vulnerable_packages")
261269
def all(self, request):
262270
"""
263-
Return all the vulnerable Package URLs.
271+
Return the Package URLs of all packages known to be vulnerable.
264272
"""
265273
vulnerable_packages = Package.objects.vulnerable().only(*PackageURL._fields).distinct()
266274
vulnerable_purls = [str(package.purl) for package in vulnerable_packages]
@@ -274,6 +282,10 @@ class Meta:
274282

275283

276284
class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet):
285+
"""
286+
Lookup for vulnerabilities affecting packages.
287+
"""
288+
277289
def get_fixed_packages_qs(self):
278290
"""
279291
Filter the packages that fixes a vulnerability
@@ -318,6 +330,10 @@ def filter_cpe(self, queryset, name, value):
318330

319331

320332
class CPEViewSet(viewsets.ReadOnlyModelViewSet):
333+
"""
334+
Lookup for vulnerabilities by CPE (https://nvd.nist.gov/products/cpe)
335+
"""
336+
321337
queryset = Vulnerability.objects.filter(
322338
vulnerabilityreference__reference_id__startswith="cpe"
323339
).distinct()
@@ -330,7 +346,7 @@ class CPEViewSet(viewsets.ReadOnlyModelViewSet):
330346
@action(detail=False, methods=["post"], throttle_scope="bulk_search_cpes")
331347
def bulk_search(self, request):
332348
"""
333-
This endpoint is used to search for vulnerabilities by more than one CPE.
349+
Lookup for vulnerabilities using many CPEs at once.
334350
"""
335351
cpes = request.data.get("cpes", []) or []
336352
if not cpes or not isinstance(cpes, list):
@@ -360,6 +376,11 @@ def filter_alias(self, queryset, name, value):
360376

361377

362378
class AliasViewSet(viewsets.ReadOnlyModelViewSet):
379+
"""
380+
Lookup for vulnerabilities by vulnerability aliases such as a CVE
381+
(https://nvd.nist.gov/general/cve-process).
382+
"""
383+
363384
queryset = Vulnerability.objects.all()
364385
serializer_class = VulnerabilitySerializer
365386
filter_backends = (filters.DjangoFilterBackend,)

vulnerabilities/forms.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
#
99

1010
from django import forms
11+
from django.core.validators import validate_email
12+
13+
from vulnerabilities.models import ApiUser
1114

1215

1316
class PackageSearchForm(forms.Form):
@@ -28,3 +31,36 @@ class VulnerabilitySearchForm(forms.Form):
2831
attrs={"placeholder": "Vulnerability id or alias such as CVE or GHSA"}
2932
),
3033
)
34+
35+
36+
class ApiUserCreationForm(forms.ModelForm):
37+
"""
38+
Support a simplified creation for API-only users directly from the UI.
39+
"""
40+
41+
class Meta:
42+
model = ApiUser
43+
fields = (
44+
"email",
45+
"first_name",
46+
"last_name",
47+
)
48+
49+
def __init__(self, *args, **kwargs):
50+
super(ApiUserCreationForm, self).__init__(*args, **kwargs)
51+
self.fields["email"].required = True
52+
53+
def save(self, commit=True):
54+
return ApiUser.objects.create_api_user(
55+
username=self.cleaned_data["email"],
56+
first_name=self.cleaned_data["first_name"],
57+
last_name=self.cleaned_data["last_name"],
58+
)
59+
60+
def clean_username(self):
61+
username = self.cleaned_data["email"]
62+
validate_email(username)
63+
return username
64+
65+
def save_m2m(self):
66+
pass

0 commit comments

Comments
 (0)