Skip to content

Commit c679856

Browse files
authored
Merge branch 'main' into add-almalinux-advisories
2 parents e82f47e + d62f377 commit c679856

File tree

135 files changed

+9921
-1931
lines changed

Some content is hidden

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

135 files changed

+9921
-1931
lines changed

CHANGELOG.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
Release notes
22
=============
33

4+
Version v34.0.0rc5
5+
-------------------
6+
7+
- Add safetydb importer.
8+
- Add missing width setting for the table in the vulnerability details UI.
9+
- Add KEV support.
10+
- Add UI template for API.
11+
- Use VersionRange.normalize to compare advisory.
12+
- Use integer column to display score.
13+
- Add support for CVSSv4 & SSVC and import the data using vulnrichment.
14+
- Add support for reference_type in the API.
15+
- Add API improvements for the package endpoint.
16+
17+
418
Version v34.0.0rc4
519
-------------------
620

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ toml==0.10.2
106106
tomli==2.0.1
107107
traitlets==5.1.1
108108
typing_extensions==4.1.1
109-
univers==30.11.0
109+
univers==30.12.0
110110
urllib3==1.26.19
111111
wcwidth==0.2.5
112112
websocket-client==0.59.0

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ install_requires =
7171

7272
#essentials
7373
packageurl-python>=0.10.5rc1
74-
univers>=30.11.0
74+
univers>=30.12.0
7575
license-expression>=21.6.14
7676

7777
# file and data formats
@@ -118,6 +118,7 @@ dev =
118118
commoncode
119119
# debug
120120
django-debug-toolbar
121+
pyinstrument
121122

122123
[options.entry_points]
123124
console_scripts =

vulnerabilities/api.py

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@
3838
class VulnerabilitySeveritySerializer(serializers.ModelSerializer):
3939
class Meta:
4040
model = VulnerabilitySeverity
41-
fields = ["value", "scoring_system", "scoring_elements"]
41+
fields = ["value", "scoring_system", "scoring_elements", "published_at"]
42+
43+
def to_representation(self, instance):
44+
data = super().to_representation(instance)
45+
published_at = data.get("published_at", None)
46+
if not published_at:
47+
data.pop("published_at")
48+
return data
4249

4350

4451
class VulnerabilityReferenceSerializer(serializers.ModelSerializer):
@@ -47,7 +54,7 @@ class VulnerabilityReferenceSerializer(serializers.ModelSerializer):
4754

4855
class Meta:
4956
model = VulnerabilityReference
50-
fields = ["reference_url", "reference_id", "scores", "url"]
57+
fields = ["reference_url", "reference_id", "reference_type", "scores", "url"]
5158

5259

5360
class BaseResourceSerializer(serializers.HyperlinkedModelSerializer):
@@ -101,6 +108,8 @@ def get_vulnerability(self, vuln):
101108

102109
purl = serializers.CharField(source="package_url")
103110

111+
is_vulnerable = serializers.BooleanField()
112+
104113
class Meta:
105114
model = Package
106115
fields = ["url", "purl", "is_vulnerable", "affected_by_vulnerabilities"]
@@ -241,21 +250,27 @@ def get_latest_non_vulnerable(self, package):
241250

242251
affected_by_vulnerabilities = serializers.SerializerMethodField("get_affected_vulnerabilities")
243252

244-
fixing_vulnerabilities = serializers.SerializerMethodField("get_fixed_vulnerabilities")
253+
fixing_vulnerabilities = serializers.SerializerMethodField("get_fixing_vulnerabilities")
254+
255+
is_vulnerable = serializers.BooleanField()
245256

246257
def get_fixed_packages(self, package):
247258
"""
248259
Return a queryset of all packages that fix a vulnerability with
249260
same type, namespace, name, subpath and qualifiers of the `package`
250261
"""
251-
return Package.objects.filter(
252-
name=package.name,
253-
namespace=package.namespace,
254-
type=package.type,
255-
qualifiers=package.qualifiers,
256-
subpath=package.subpath,
257-
packagerelatedvulnerability__fix=True,
258-
).distinct()
262+
return (
263+
Package.objects.filter(
264+
name=package.name,
265+
namespace=package.namespace,
266+
type=package.type,
267+
qualifiers=package.qualifiers,
268+
subpath=package.subpath,
269+
packagerelatedvulnerability__fix=True,
270+
)
271+
.with_is_vulnerable()
272+
.distinct()
273+
)
259274

260275
def get_vulnerabilities_for_a_package(self, package, fix) -> dict:
261276
"""
@@ -278,7 +293,7 @@ def get_vulnerabilities_for_a_package(self, package, fix) -> dict:
278293
context={"request": self.context["request"]},
279294
).data
280295

281-
def get_fixed_vulnerabilities(self, package) -> dict:
296+
def get_fixing_vulnerabilities(self, package) -> dict:
282297
"""
283298
Return a mapping of vulnerabilities fixed in the given `package`.
284299
"""
@@ -315,12 +330,15 @@ class Meta:
315330
"version",
316331
"qualifiers",
317332
"subpath",
333+
"is_vulnerable",
318334
"next_non_vulnerable_version",
319335
"latest_non_vulnerable_version",
320336
"affected_by_vulnerabilities",
321337
"fixing_vulnerabilities",
322338
]
323339

340+
is_vulnerable = serializers.BooleanField()
341+
324342

325343
class PackageFilterSet(filters.FilterSet):
326344
purl = filters.CharFilter(method="filter_purl")
@@ -383,6 +401,9 @@ class PackageViewSet(viewsets.ReadOnlyModelViewSet):
383401
filterset_class = PackageFilterSet
384402
throttle_classes = [StaffUserRateThrottle, AnonRateThrottle]
385403

404+
def get_queryset(self):
405+
return super().get_queryset().with_is_vulnerable()
406+
386407
@extend_schema(
387408
request=PackageBulkSearchRequestSerializer,
388409
responses={200: PackageSerializer(many=True)},
@@ -429,6 +450,7 @@ def bulk_search(self, request):
429450
Package.objects.filter(plain_package_url__in=plain_purls)
430451
.order_by("plain_package_url")
431452
.distinct("plain_package_url")
453+
.with_is_vulnerable()
432454
)
433455

434456
if not purl_only:
@@ -442,7 +464,7 @@ def bulk_search(self, request):
442464
vulnerable_purls = [str(package.plain_package_url) for package in vulnerable_purls]
443465
return Response(data=vulnerable_purls)
444466

445-
query = Package.objects.filter(package_url__in=purls).distinct()
467+
query = Package.objects.filter(package_url__in=purls).distinct().with_is_vulnerable()
446468

447469
if not purl_only:
448470
return Response(PackageSerializer(query, many=True, context={"request": request}).data)
@@ -456,7 +478,9 @@ def all(self, request):
456478
"""
457479
Return the Package URLs of all packages known to be vulnerable.
458480
"""
459-
vulnerable_packages = Package.objects.vulnerable().only("package_url").distinct()
481+
vulnerable_packages = (
482+
Package.objects.vulnerable().only("package_url").distinct().with_is_vulnerable()
483+
)
460484
vulnerable_purls = [str(package.package_url) for package in vulnerable_packages]
461485
return Response(vulnerable_purls)
462486

@@ -487,11 +511,8 @@ def lookup(self, request):
487511
validated_data = serializer.validated_data
488512
purl = validated_data.get("purl")
489513

490-
return Response(
491-
PackageSerializer(
492-
Package.objects.for_purls([purl]), many=True, context={"request": request}
493-
).data
494-
)
514+
qs = self.get_queryset().for_purls([purl]).with_is_vulnerable()
515+
return Response(PackageSerializer(qs, many=True, context={"request": request}).data)
495516

496517
@extend_schema(
497518
request=PackageurlListSerializer,
@@ -522,7 +543,7 @@ def bulk_lookup(self, request):
522543

523544
return Response(
524545
PackageSerializer(
525-
Package.objects.for_purls(purls),
546+
Package.objects.for_purls(purls).with_is_vulnerable(),
526547
many=True,
527548
context={"request": request},
528549
).data
@@ -540,33 +561,47 @@ class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet):
540561
Lookup for vulnerabilities affecting packages.
541562
"""
542563

564+
queryset = Vulnerability.objects.all()
565+
543566
def get_fixed_packages_qs(self):
544567
"""
545568
Filter the packages that fixes a vulnerability
546569
on fields like name, namespace and type.
547570
"""
548-
package_filter_data = {"packagerelatedvulnerability__fix": True}
571+
return self.get_packages_qs().filter(packagerelatedvulnerability__fix=True)
549572

573+
def get_packages_qs(self):
574+
"""
575+
Filter the packages on type, namespace and name.
576+
"""
550577
query_params = self.request.query_params
551-
for field_name in ["name", "namespace", "type"]:
552-
value = query_params.get(field_name)
553-
if value:
578+
package_filter_data = {}
579+
for field_name in ("type", "namespace", "name"):
580+
if value := query_params.get(field_name):
554581
package_filter_data[field_name] = value
555582

556-
return PackageFilterSet(package_filter_data).qs
583+
return PackageFilterSet(package_filter_data).qs.with_is_vulnerable()
557584

558585
def get_queryset(self):
559586
"""
560587
Assign filtered packages queryset from `get_fixed_packages_qs`
561588
to a custom attribute `filtered_fixed_packages`
562589
"""
563-
return Vulnerability.objects.prefetch_related(
564-
"weaknesses",
565-
Prefetch(
566-
"packages",
567-
queryset=self.get_fixed_packages_qs(),
568-
to_attr="filtered_fixed_packages",
569-
),
590+
return (
591+
super()
592+
.get_queryset()
593+
.prefetch_related(
594+
Prefetch(
595+
"packages",
596+
queryset=self.get_packages_qs(),
597+
),
598+
"weaknesses",
599+
Prefetch(
600+
"packages",
601+
queryset=self.get_fixed_packages_qs(),
602+
to_attr="filtered_fixed_packages",
603+
),
604+
)
570605
)
571606

572607
serializer_class = VulnerabilitySerializer

vulnerabilities/import_runner.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,23 @@ def do_import(self, advisories) -> None:
7070
if advisory.date_imported:
7171
continue
7272
logger.info(f"Processing advisory: {advisory!r}")
73+
advisory_data = None
74+
inferences = None
7375
try:
74-
inferences = advisory_importer.get_inferences(
75-
advisory_data=advisory.to_advisory_data()
76-
)
76+
advisory_data = advisory.to_advisory_data()
77+
inferences = advisory_importer.get_inferences(advisory_data=advisory_data)
7778
process_inferences(
7879
inferences=inferences,
7980
advisory=advisory,
8081
improver_name=importer_name,
8182
)
82-
except Exception as e:
83-
logger.info(f"Failed to process advisory: {advisory!r} with error {e!r}")
83+
except Exception:
84+
from pprint import pformat
85+
86+
logger.warning(
87+
f"Failed to process advisory:\n{pformat(advisory_data.to_dict())}\n\n"
88+
f"with error:\n{traceback_format_exc()}\n\n"
89+
)
8490
logger.info("Finished importing using %s.", advisory_importer.__class__.qualified_name)
8591

8692
def process_advisories(
@@ -181,16 +187,23 @@ def process_inferences(inferences: List[Inference], advisory: Advisory, improver
181187
reference=reference,
182188
vulnerability=vulnerability,
183189
)
184-
190+
updated = False
185191
for severity in ref.severities:
186-
_vs, updated = VulnerabilitySeverity.objects.update_or_create(
187-
scoring_system=severity.system.identifier,
188-
reference=reference,
189-
defaults={
190-
"value": str(severity.value),
191-
"scoring_elements": str(severity.scoring_elements),
192-
},
193-
)
192+
try:
193+
published_at = str(severity.published_at) if severity.published_at else None
194+
_vs, updated = VulnerabilitySeverity.objects.update_or_create(
195+
scoring_system=severity.system.identifier,
196+
reference=reference,
197+
defaults={
198+
"value": str(severity.value),
199+
"scoring_elements": str(severity.scoring_elements),
200+
"published_at": published_at,
201+
},
202+
)
203+
except:
204+
logger.error(
205+
f"Failed to create VulnerabilitySeverity for: {severity} with error:\n{traceback_format_exc()}"
206+
)
194207
if updated:
195208
logger.info(
196209
f"Severity updated for reference {ref!r} to value: {severity.value!r} "

vulnerabilities/importer.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,17 @@ class VulnerabilitySeverity:
5252
system: ScoringSystem
5353
value: str
5454
scoring_elements: str = ""
55+
published_at: Optional[datetime.datetime] = None
5556

5657
def to_dict(self):
58+
published_at_dict = (
59+
{"published_at": self.published_at.isoformat()} if self.published_at else {}
60+
)
5761
return {
5862
"system": self.system.identifier,
5963
"value": self.value,
6064
"scoring_elements": self.scoring_elements,
65+
**published_at_dict,
6166
}
6267

6368
@classmethod
@@ -70,12 +75,14 @@ def from_dict(cls, severity: dict):
7075
system=SCORING_SYSTEMS[severity["system"]],
7176
value=severity["value"],
7277
scoring_elements=severity.get("scoring_elements", ""),
78+
published_at=severity.get("published_at"),
7379
)
7480

7581

7682
@dataclasses.dataclass(order=True)
7783
class Reference:
7884
reference_id: str = ""
85+
reference_type: str = ""
7986
url: str = ""
8087
severities: List[VulnerabilitySeverity] = dataclasses.field(default_factory=list)
8188

@@ -85,11 +92,17 @@ def __post_init__(self):
8592

8693
def normalized(self):
8794
severities = sorted(self.severities)
88-
return Reference(reference_id=self.reference_id, url=self.url, severities=severities)
95+
return Reference(
96+
reference_id=self.reference_id,
97+
url=self.url,
98+
severities=severities,
99+
reference_type=self.reference_type,
100+
)
89101

90102
def to_dict(self):
91103
return {
92104
"reference_id": self.reference_id,
105+
"reference_type": self.reference_type,
93106
"url": self.url,
94107
"severities": [severity.to_dict() for severity in self.severities],
95108
}
@@ -98,6 +111,7 @@ def to_dict(self):
98111
def from_dict(cls, ref: dict):
99112
return cls(
100113
reference_id=ref["reference_id"],
114+
reference_type=ref["reference_type"],
101115
url=ref["url"],
102116
severities=[
103117
VulnerabilitySeverity.from_dict(severity) for severity in ref["severities"]

0 commit comments

Comments
 (0)