Skip to content

Commit 7cfb1e0

Browse files
authored
Merge branch 'main' into feature/update-workflow
2 parents a18b569 + 3fb3e75 commit 7cfb1e0

Some content is hidden

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

50 files changed

+3766
-3272
lines changed

CHANGELOG.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@ Release notes
22
=============
33

44

5+
Version v36.0.0
6+
---------------------
7+
8+
- Add indexes for models https://github.com/aboutcode-org/vulnerablecode/pull/1701
9+
- Add fixed by package in V2 API https://github.com/aboutcode-org/vulnerablecode/pull/1706
10+
- Add tests for num queries for views https://github.com/aboutcode-org/vulnerablecode/pull/1730
11+
- Add postgresql conf in docker-compose https://github.com/aboutcode-org/vulnerablecode/pull/1733
12+
- Add default postgresql.conf for local docker build https://github.com/aboutcode-org/vulnerablecode/pull/1735
13+
- Add models for CodeFix https://github.com/aboutcode-org/vulnerablecode/pull/1704
14+
- Migrate Alpine Linux importer to aboutcode pipeline https://github.com/aboutcode-org/vulnerablecode/pull/1737
15+
- VCIO-next: Allow CVSS3.1 Severities in NVD https://github.com/aboutcode-org/vulnerablecode/pull/1738
16+
- Add Pipeline to add missing CVSSV3.1 scores https://github.com/aboutcode-org/vulnerablecode/pull/1740
17+
- Add description and reference to the latest release on the homepage https://github.com/aboutcode-org/vulnerablecode/pull/1743
18+
- Use proper apk package type for Alpine https://github.com/aboutcode-org/vulnerablecode/pull/1739
19+
- Optimize vulnerabilities view https://github.com/aboutcode-org/vulnerablecode/pull/1728
20+
- Add CWE support in multiple importers https://github.com/aboutcode-org/vulnerablecode/pull/1526
21+
- Fast content ID migration https://github.com/aboutcode-org/vulnerablecode/pull/1795
22+
- Add captcha for user signup https://github.com/aboutcode-org/vulnerablecode/pull/1822
23+
- Move the package search box to the top by @keshav-space in https://github.com/aboutcode-org/vulnerablecode/pull/1832
24+
25+
526
Version v35.1.0
627
---------------------
728

requirements.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@ charset-normalizer==2.0.12
2020
click==8.1.2
2121
coreapi==2.3.3
2222
coreschema==0.0.4
23-
cryptography==43.0.1
23+
cryptography==44.0.1
2424
crispy-bootstrap4==2024.1
2525
cvss
2626
cwe2==3.0.0
2727
dateparser==1.1.1
2828
decorator==5.1.1
2929
defusedxml==0.7.1
3030
distro==1.7.0
31-
Django==4.2.17
31+
Django==4.2.20
3232
django-crispy-forms==2.3
3333
django-environ==0.11.2
3434
django-extensions==3.2.3
3535
django-filter==24.3
36+
django-recaptcha==4.0.0
3637
django-widget-tweaks==1.5.0
3738
djangorestframework==3.15.2
3839
doc8==0.11.1
@@ -56,7 +57,7 @@ ipython==8.10.0
5657
isort==5.10.1
5758
itypes==1.2.0
5859
jedi==0.18.1
59-
Jinja2==3.1.5
60+
Jinja2==3.1.6
6061
jsonschema==3.2.0
6162
license-expression==30.3.1
6263
lxml==4.9.1

setup.cfg

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = vulnerablecode
3-
version = 35.1.0
3+
version = 36.0.0
44
license = Apache-2.0 AND CC-BY-SA-4.0
55

66
# description must be on ONE line https://github.com/pypa/setuptools/issues/1390
@@ -99,6 +99,8 @@ install_requires =
9999
python-dotenv
100100
texttable
101101

102+
django-recaptcha>=4.0.0
103+
102104

103105
[options.extras_require]
104106
dev =

vulnerabilities/forms.py

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

1010
from django import forms
1111
from django.core.validators import validate_email
12+
from django_recaptcha.fields import ReCaptchaField
13+
from django_recaptcha.widgets import ReCaptchaV2Checkbox
1214

1315
from vulnerabilities.models import ApiUser
1416

@@ -38,6 +40,10 @@ class ApiUserCreationForm(forms.ModelForm):
3840
Support a simplified creation for API-only users directly from the UI.
3941
"""
4042

43+
captcha = ReCaptchaField(
44+
error_messages={"required": ("Captcha is required")}, widget=ReCaptchaV2Checkbox
45+
)
46+
4147
class Meta:
4248
model = ApiUser
4349
fields = (

vulnerabilities/import_runner.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from django.core.exceptions import ValidationError
1717
from django.db import transaction
18+
from django.db.models.query import QuerySet
1819

1920
from vulnerabilities.importer import AdvisoryData
2021
from vulnerabilities.importer import Importer
@@ -96,23 +97,29 @@ def process_advisories(
9697
Insert advisories into the database
9798
Return the number of inserted advisories.
9899
"""
100+
from vulnerabilities.pipes.advisory import get_or_create_aliases
101+
from vulnerabilities.utils import compute_content_id
102+
99103
count = 0
100104
advisories = []
101105
for data in advisory_datas:
106+
content_id = compute_content_id(advisory_data=data)
102107
try:
108+
aliases = get_or_create_aliases(aliases=data.aliases)
103109
obj, created = Advisory.objects.get_or_create(
104-
aliases=data.aliases,
105-
summary=data.summary,
106-
affected_packages=[pkg.to_dict() for pkg in data.affected_packages],
107-
references=[ref.to_dict() for ref in data.references],
108-
date_published=data.date_published,
109-
weaknesses=data.weaknesses,
110+
unique_content_id=content_id,
111+
url=data.url,
110112
defaults={
113+
"summary": data.summary,
114+
"affected_packages": [pkg.to_dict() for pkg in data.affected_packages],
115+
"references": [ref.to_dict() for ref in data.references],
116+
"date_published": data.date_published,
117+
"weaknesses": data.weaknesses,
111118
"created_by": importer_name,
112119
"date_collected": datetime.datetime.now(tz=datetime.timezone.utc),
113120
},
114-
url=data.url,
115121
)
122+
obj.aliases.add(*aliases)
116123
if not obj.date_imported:
117124
advisories.append(obj)
118125
except Exception as e:
@@ -148,6 +155,8 @@ def process_inferences(inferences: List[Inference], advisory: Advisory, improver
148155
erroneous. Also, the atomic transaction for every advisory and its
149156
inferences makes sure that date_imported of advisory is consistent.
150157
"""
158+
from vulnerabilities.pipes.advisory import get_or_create_aliases
159+
151160
inferences_processed_count = 0
152161

153162
if not inferences:
@@ -157,9 +166,10 @@ def process_inferences(inferences: List[Inference], advisory: Advisory, improver
157166
logger.info(f"Improving advisory id: {advisory.id}")
158167

159168
for inference in inferences:
169+
aliases = get_or_create_aliases(inference.aliases)
160170
vulnerability = get_or_create_vulnerability_and_aliases(
161171
vulnerability_id=inference.vulnerability_id,
162-
aliases=inference.aliases,
172+
aliases=aliases,
163173
summary=inference.summary,
164174
advisory=advisory,
165175
)
@@ -265,14 +275,13 @@ def create_valid_vulnerability_reference(url, reference_id=None):
265275

266276

267277
def get_or_create_vulnerability_and_aliases(
268-
aliases: List[str], vulnerability_id=None, summary=None, advisory=None
278+
aliases: QuerySet, vulnerability_id=None, summary=None, advisory=None
269279
):
270280
"""
271281
Get or create vulnerabilitiy and aliases such that all existing and new
272282
aliases point to the same vulnerability
273283
"""
274-
aliases = set(alias.strip() for alias in aliases if alias and alias.strip())
275-
new_alias_names, existing_vulns = get_vulns_for_aliases_and_get_new_aliases(aliases)
284+
new_aliases, existing_vulns = get_vulns_for_aliases_and_get_new_aliases(aliases)
276285

277286
# All aliases must point to the same vulnerability
278287
vulnerability = None
@@ -310,11 +319,11 @@ def get_or_create_vulnerability_and_aliases(
310319
# f"Inconsistent summary for {vulnerability.vulnerability_id}. "
311320
# f"Existing: {vulnerability.summary!r}, provided: {summary!r}"
312321
# )
313-
associate_vulnerability_with_aliases(vulnerability=vulnerability, aliases=new_alias_names)
322+
associate_vulnerability_with_aliases(vulnerability=vulnerability, aliases=new_aliases)
314323
else:
315324
try:
316325
vulnerability = create_vulnerability_and_add_aliases(
317-
aliases=new_alias_names, summary=summary
326+
aliases=new_aliases, summary=summary
318327
)
319328
importer_name = get_importer_name(advisory)
320329
VulnerabilityChangeLog.log_import(
@@ -324,24 +333,22 @@ def get_or_create_vulnerability_and_aliases(
324333
)
325334
except Exception as e:
326335
logger.error(
327-
f"Cannot create vulnerability with summary {summary!r} and {new_alias_names!r} {e!r}.\n{traceback_format_exc()}."
336+
f"Cannot create vulnerability with summary {summary!r} and {new_aliases!r} {e!r}.\n{traceback_format_exc()}."
328337
)
329338
return
330339

331340
return vulnerability
332341

333342

334-
def get_vulns_for_aliases_and_get_new_aliases(aliases):
343+
def get_vulns_for_aliases_and_get_new_aliases(aliases: QuerySet):
335344
"""
336345
Return ``new_aliases`` that are not in the database and
337346
``existing_vulns`` that point to the given ``aliases``.
338347
"""
339-
new_aliases = set(aliases)
340-
existing_vulns = set()
341-
for alias in Alias.objects.filter(alias__in=aliases):
342-
existing_vulns.add(alias.vulnerability)
343-
new_aliases.remove(alias.alias)
344-
return new_aliases, existing_vulns
348+
new_aliases = aliases.filter(vulnerability__isnull=True)
349+
existing_vulns = [alias.vulnerability for alias in aliases.filter(vulnerability__isnull=False)]
350+
351+
return new_aliases, list(set(existing_vulns))
345352

346353

347354
@transaction.atomic
@@ -360,7 +367,5 @@ def create_vulnerability_and_add_aliases(aliases, summary):
360367

361368

362369
def associate_vulnerability_with_aliases(aliases, vulnerability):
363-
for alias_name in aliases:
364-
alias = Alias(alias=alias_name, vulnerability=vulnerability)
365-
alias.save()
366-
logger.info(f"New alias for {vulnerability!r}: {alias_name}")
370+
aliases.update(vulnerability=vulnerability)
371+
logger.info(f"New alias for {vulnerability!r}: {aliases}")

vulnerabilities/importer.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import dataclasses
1111
import datetime
12+
import functools
1213
import logging
1314
import os
1415
import shutil
@@ -46,7 +47,8 @@
4647
logger = logging.getLogger(__name__)
4748

4849

49-
@dataclasses.dataclass(order=True)
50+
@dataclasses.dataclass(eq=True)
51+
@functools.total_ordering
5052
class VulnerabilitySeverity:
5153
# FIXME: this should be named scoring_system, like in the model
5254
system: ScoringSystem
@@ -55,15 +57,26 @@ class VulnerabilitySeverity:
5557
published_at: Optional[datetime.datetime] = None
5658

5759
def to_dict(self):
58-
published_at_dict = (
59-
{"published_at": self.published_at.isoformat()} if self.published_at else {}
60-
)
61-
return {
60+
data = {
6261
"system": self.system.identifier,
6362
"value": self.value,
6463
"scoring_elements": self.scoring_elements,
65-
**published_at_dict,
6664
}
65+
if self.published_at:
66+
if isinstance(self.published_at, datetime.datetime):
67+
data["published_at"] = self.published_at.isoformat()
68+
else:
69+
data["published_at"] = self.published_at
70+
return data
71+
72+
def __lt__(self, other):
73+
if not isinstance(other, VulnerabilitySeverity):
74+
return NotImplemented
75+
return self._cmp_key() < other._cmp_key()
76+
77+
# TODO: Add cache
78+
def _cmp_key(self):
79+
return (self.system.identifier, self.value, self.scoring_elements, self.published_at)
6780

6881
@classmethod
6982
def from_dict(cls, severity: dict):
@@ -79,7 +92,8 @@ def from_dict(cls, severity: dict):
7992
)
8093

8194

82-
@dataclasses.dataclass(order=True)
95+
@dataclasses.dataclass(eq=True)
96+
@functools.total_ordering
8397
class Reference:
8498
reference_id: str = ""
8599
reference_type: str = ""
@@ -89,28 +103,31 @@ class Reference:
89103
def __post_init__(self):
90104
if not self.url:
91105
raise TypeError("Reference must have a url")
106+
if self.reference_id and not isinstance(self.reference_id, str):
107+
self.reference_id = str(self.reference_id)
92108

93-
def normalized(self):
94-
severities = sorted(self.severities)
95-
return Reference(
96-
reference_id=self.reference_id,
97-
url=self.url,
98-
severities=severities,
99-
reference_type=self.reference_type,
100-
)
109+
def __lt__(self, other):
110+
if not isinstance(other, Reference):
111+
return NotImplemented
112+
return self._cmp_key() < other._cmp_key()
113+
114+
# TODO: Add cache
115+
def _cmp_key(self):
116+
return (self.reference_id, self.reference_type, self.url, tuple(self.severities))
101117

102118
def to_dict(self):
119+
"""Return a normalized dictionary representation"""
103120
return {
104121
"reference_id": self.reference_id,
105122
"reference_type": self.reference_type,
106123
"url": self.url,
107-
"severities": [severity.to_dict() for severity in self.severities],
124+
"severities": [severity.to_dict() for severity in sorted(self.severities)],
108125
}
109126

110127
@classmethod
111128
def from_dict(cls, ref: dict):
112129
return cls(
113-
reference_id=ref["reference_id"],
130+
reference_id=str(ref["reference_id"]),
114131
reference_type=ref.get("reference_type") or "",
115132
url=ref["url"],
116133
severities=[
@@ -140,7 +157,8 @@ class NoAffectedPackages(Exception):
140157
"""
141158

142159

143-
@dataclasses.dataclass(order=True, frozen=True)
160+
@functools.total_ordering
161+
@dataclasses.dataclass(eq=True)
144162
class AffectedPackage:
145163
"""
146164
Relate a Package URL with a range of affected versions and a fixed version.
@@ -170,6 +188,19 @@ def get_fixed_purl(self):
170188
raise ValueError(f"Affected Package {self.package!r} does not have a fixed version")
171189
return update_purl_version(purl=self.package, version=str(self.fixed_version))
172190

191+
def __lt__(self, other):
192+
if not isinstance(other, AffectedPackage):
193+
return NotImplemented
194+
return self._cmp_key() < other._cmp_key()
195+
196+
# TODO: Add cache
197+
def _cmp_key(self):
198+
return (
199+
str(self.package),
200+
str(self.affected_version_range or ""),
201+
str(self.fixed_version or ""),
202+
)
203+
173204
@classmethod
174205
def merge(
175206
cls, affected_packages: Iterable

0 commit comments

Comments
 (0)